From 9a141b7a2b426faf50f96f24dccfaece87ed83b3 Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Mon, 20 Jul 2026 10:11:27 +0800 Subject: [PATCH] fix(security): preserve YAML block scalar text Signed-off-by: dongmucat <1127093059@qq.com> --- .../skill/metadata/SkillMetadataParser.java | 141 +++--------------- .../metadata/SkillMetadataParserTest.java | 17 +++ 2 files changed, 35 insertions(+), 123 deletions(-) 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 072df7c9..5a8736e5 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 @@ -4,7 +4,10 @@ import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.events.CollectionStartEvent; +import org.yaml.snakeyaml.events.ScalarEvent; +import java.io.StringReader; import java.util.LinkedHashMap; import java.util.Map; @@ -63,11 +66,9 @@ 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"); - } try { Yaml yaml = newSafeYamlParser(); + rejectExplicitYamlTags(yaml, yamlContent); Object parsed = yaml.load(yamlContent); if (!(parsed instanceof Map)) { throw new DomainBadRequestException("error.skill.metadata.yaml.notMap"); @@ -97,6 +98,20 @@ public class SkillMetadataParser { return new Yaml(new SafeConstructor(options)); } + private void rejectExplicitYamlTags(Yaml yaml, String yamlContent) { + for (var event : yaml.parse(new StringReader(yamlContent))) { + boolean taggedScalar = event instanceof ScalarEvent scalarEvent && scalarEvent.getTag() != null; + boolean taggedCollection = event instanceof CollectionStartEvent collectionEvent + && collectionEvent.getTag() != null; + if (taggedScalar || taggedCollection) { + throw new DomainBadRequestException( + "error.skill.metadata.yaml.invalid", + "Explicit YAML tags are not allowed" + ); + } + } + } + private void validateFrontmatterCodePointLimit(String yamlContent) { if (yamlContent.codePointCount(0, yamlContent.length()) > FRONTMATTER_CODE_POINT_LIMIT) { throw new DomainBadRequestException( @@ -116,126 +131,6 @@ public class SkillMetadataParser { || message.contains("found duplicate key"); } - private boolean containsExplicitYamlTag(String yamlContent) { - return yamlContent.lines() - .map(String::trim) - .filter(line -> !line.isEmpty() && !line.startsWith("#")) - .anyMatch(this::containsExplicitYamlTagInLine); - } - - private boolean containsExplicitYamlTagInLine(String line) { - String candidate = stripLeadingIndicatorsAndAnchors(line); - if (startsWithExplicitYamlTag(candidate)) { - return true; - } - if (candidate.startsWith("[") || candidate.startsWith("{")) { - return containsFlowExplicitYamlTag(candidate); - } - - int separatorIndex = line.indexOf(':'); - if (separatorIndex <= 0) { - return false; - } - - String value = stripLeadingAnchors(line.substring(separatorIndex + 1)); - if (startsWithExplicitYamlTag(value)) { - return true; - } - return (value.startsWith("[") || value.startsWith("{")) && containsFlowExplicitYamlTag(value); - } - - private String stripLeadingIndicatorsAndAnchors(String line) { - String candidate = line.trim(); - boolean advanced; - do { - advanced = false; - if (candidate.startsWith("- ") || candidate.startsWith("? ") || candidate.startsWith(": ")) { - candidate = candidate.substring(1).trim(); - advanced = true; - continue; - } - - String withoutAnchor = stripLeadingAnchors(candidate); - if (!withoutAnchor.equals(candidate)) { - candidate = withoutAnchor; - advanced = true; - } - } while (advanced); - return candidate; - } - - private String stripLeadingAnchors(String value) { - String candidate = value.trim(); - while (candidate.startsWith("&") && candidate.length() > 1) { - int end = 1; - while (end < candidate.length() - && !Character.isWhitespace(candidate.charAt(end)) - && !isYamlFlowDelimiter(candidate.charAt(end))) { - end++; - } - if (end == 1) { - break; - } - candidate = candidate.substring(end).trim(); - } - return candidate; - } - - private boolean startsWithExplicitYamlTag(String value) { - return value.startsWith("!") && value.length() > 1 && !Character.isWhitespace(value.charAt(1)); - } - - private boolean containsFlowExplicitYamlTag(String value) { - int flowDepth = 0; - char quote = '\0'; - for (int i = 0; i < value.length(); i++) { - char current = value.charAt(i); - if (quote != '\0') { - if (current == quote && !isEscapedDoubleQuote(value, i, quote)) { - quote = '\0'; - } - continue; - } - if (current == '\'' || current == '"') { - quote = current; - continue; - } - - if (current == '[' || current == '{') { - flowDepth++; - if (startsWithExplicitYamlTag(stripLeadingAnchors(value.substring(i + 1)))) { - return true; - } - continue; - } - if (current == ']' || current == '}') { - flowDepth = Math.max(0, flowDepth - 1); - continue; - } - if (flowDepth > 0 - && (current == ',' || current == ':') - && startsWithExplicitYamlTag(stripLeadingAnchors(value.substring(i + 1)))) { - return true; - } - } - return false; - } - - private boolean isEscapedDoubleQuote(String value, int index, char quote) { - if (quote != '"' || index == 0) { - return false; - } - int backslashCount = 0; - for (int i = index - 1; i >= 0 && value.charAt(i) == '\\'; i--) { - backslashCount++; - } - return backslashCount % 2 == 1; - } - - private boolean isYamlFlowDelimiter(char current) { - return current == '[' || current == ']' || current == '{' || current == '}' || current == ','; - } - private Map parseLooseFrontmatter(String yamlContent) { Map values = new LinkedHashMap<>(); for (String rawLine : yamlContent.split("\\R")) { 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 c4befbb3..daba1e09 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 @@ -343,6 +343,23 @@ class SkillMetadataParserTest { assertEquals("Avoid CSS, !important when possible", metadata.description()); } + @Test + void allowsExclamationTextInsideBlockScalars() { + String content = """ + --- + name: css-guidance + description: |- + !important is literal prose, not a YAML tag + version: 1.0.0 + --- + Body + """; + + SkillMetadata metadata = parser.parse(content); + + assertEquals("!important is literal prose, not a YAML tag", metadata.description()); + } + @Test void testAllowsColonInDescriptionWithoutStrictYamlQuoting() { String content = """