diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java index 87cad7c4..072df7c9 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java @@ -62,6 +62,7 @@ public class SkillMetadataParser { } private Map parseFrontmatter(String yamlContent) { + validateFrontmatterCodePointLimit(yamlContent); if (containsExplicitYamlTag(yamlContent)) { throw new DomainBadRequestException("error.skill.metadata.yaml.invalid", "Explicit YAML tags are not allowed"); } @@ -77,6 +78,9 @@ public class SkillMetadataParser { } catch (DomainBadRequestException exception) { throw exception; } catch (Exception exception) { + if (isLoaderConstraintException(exception)) { + throw new DomainBadRequestException("error.skill.metadata.yaml.invalid", exception.getMessage()); + } Map fallback = parseLooseFrontmatter(yamlContent); if (!fallback.isEmpty()) { return fallback; @@ -93,6 +97,25 @@ public class SkillMetadataParser { return new Yaml(new SafeConstructor(options)); } + private void validateFrontmatterCodePointLimit(String yamlContent) { + if (yamlContent.codePointCount(0, yamlContent.length()) > FRONTMATTER_CODE_POINT_LIMIT) { + throw new DomainBadRequestException( + "error.skill.metadata.yaml.invalid", + "Frontmatter exceeds the supported size" + ); + } + } + + private boolean isLoaderConstraintException(Exception exception) { + String message = exception.getMessage(); + if (message == null) { + return false; + } + return message.contains("exceeds the limit") + || message.contains("Nesting Depth exceeded") + || message.contains("found duplicate key"); + } + private boolean containsExplicitYamlTag(String yamlContent) { return yamlContent.lines() .map(String::trim) @@ -231,6 +254,12 @@ public class SkillMetadataParser { if (key.isEmpty()) { continue; } + if (values.containsKey(key)) { + throw new DomainBadRequestException( + "error.skill.metadata.yaml.invalid", + "Duplicate YAML keys are not allowed" + ); + } values.put(key, stripWrappingQuotes(value)); } diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java index 98d6dac3..c4befbb3 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java @@ -181,6 +181,64 @@ class SkillMetadataParserTest { assertEquals("1.0.0", metadata.version()); } + @Test + void rejectsOversizedFrontmatterInsteadOfFallingBackToLooseParsing() { + String longDescription = "x".repeat(70_000); + String content = """ + --- + name: oversized-skill + description: [unclosed bracket %s + version: 1.0.0 + --- + Body + """.formatted(longDescription); + + DomainBadRequestException exception = assertThrows( + DomainBadRequestException.class, + () -> parser.parse(content) + ); + assertEquals("error.skill.metadata.yaml.invalid", exception.messageCode()); + } + + @Test + void rejectsDuplicateKeysInsteadOfFallingBackToLooseParsing() { + String content = """ + --- + name: original-skill + name: overwritten-skill + description: Duplicate keys should not be accepted + version: 1.0.0 + --- + Body + """; + + DomainBadRequestException exception = assertThrows( + DomainBadRequestException.class, + () -> parser.parse(content) + ); + assertEquals("error.skill.metadata.yaml.invalid", exception.messageCode()); + } + + @Test + void rejectsExcessiveNestingInsteadOfFallingBackToLooseParsing() { + String nestedValue = "[".repeat(25) + "value" + "]".repeat(25); + String content = """ + --- + name: deeply-nested-skill + description: Deeply nested frontmatter should not be accepted + metadata: %s + version: 1.0.0 + --- + Body + """.formatted(nestedValue); + + DomainBadRequestException exception = assertThrows( + DomainBadRequestException.class, + () -> parser.parse(content) + ); + assertEquals("error.skill.metadata.yaml.invalid", exception.messageCode()); + } + @Test void rejectsExplicitYamlTagsInsteadOfFallingBackToLooseParsing() { String content = """