fix(security): enforce YAML fallback limits

Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
dongmucat 2026-07-07 11:09:53 +08:00
parent f4fec3fe22
commit 4ff546b23d
2 changed files with 87 additions and 0 deletions

View file

@ -62,6 +62,7 @@ public class SkillMetadataParser {
}
private Map<String, Object> 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<String, Object> 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));
}

View file

@ -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 = """