mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-09 03:17:52 +00:00
fix(security): preserve YAML block scalar text
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
4ff546b23d
commit
9a141b7a2b
2 changed files with 35 additions and 123 deletions
|
|
@ -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<String, Object> 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<String, Object> parseLooseFrontmatter(String yamlContent) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
for (String rawLine : yamlContent.split("\\R")) {
|
||||
|
|
|
|||
|
|
@ -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 = """
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue