fix(ISSUE-64): restrict compliance evidenceUrl schemes

Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
dongmucat 2026-06-29 09:49:27 +08:00
parent 1b5a8ab769
commit eb2db152fd
3 changed files with 67 additions and 5 deletions

View file

@ -5,12 +5,12 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.regex.Pattern;
/**
@ -23,6 +23,8 @@ public class SkillComplianceMetadataService {
private static final int MAX_STANDARD_VERSION_LENGTH = 32;
private static final int MAX_CONTROL_TITLE_LENGTH = 200;
private static final Pattern CONTROL_ID_PATTERN = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$");
private static final Set<String> ALLOWED_EVIDENCE_URL_SCHEMES = Set.of("http", "https");
private static final String EVIDENCE_URL_ERROR = "evidenceUrl must use an http or https URI";
private static final Set<String> ALLOWED_KEYS = Set.of(
"standard",
"standardVersion",
@ -221,18 +223,21 @@ public class SkillComplianceMetadataService {
return null;
}
if (!(rawValue instanceof String value) || value.isBlank()) {
errors.add("x-astron-compliance[" + index + "].evidenceUrl must be an absolute URI");
errors.add("x-astron-compliance[" + index + "]." + EVIDENCE_URL_ERROR);
return null;
}
try {
URI uri = URI.create(value.trim());
if (!uri.isAbsolute()) {
errors.add("x-astron-compliance[" + index + "].evidenceUrl must be an absolute URI");
String scheme = uri.getScheme();
if (!uri.isAbsolute()
|| scheme == null
|| !ALLOWED_EVIDENCE_URL_SCHEMES.contains(scheme.toLowerCase(Locale.ROOT))) {
errors.add("x-astron-compliance[" + index + "]." + EVIDENCE_URL_ERROR);
return null;
}
return uri.toString();
} catch (IllegalArgumentException ex) {
errors.add("x-astron-compliance[" + index + "].evidenceUrl must be an absolute URI");
errors.add("x-astron-compliance[" + index + "]." + EVIDENCE_URL_ERROR);
return null;
}
}

View file

@ -84,6 +84,35 @@ class SkillComplianceMetadataServiceTest {
assertThat(result.errors()).contains("x-astron-compliance must be a non-empty array");
}
@Test
void parseFrontmatter_rejectsUnsafeEvidenceUrlSchemes() {
Map<String, Object> frontmatter = Map.of(
"x-astron-compliance",
List.of(
Map.of(
"standard", "gdpr",
"standardVersion", "2024",
"controlId", "Article-17",
"evidenceUrl", "javascript:alert(1)"
),
Map.of(
"standard", "soc2",
"standardVersion", "2017",
"controlId", "CC6.1",
"evidenceUrl", "data:text/html;base64,SGk="
)
)
);
SkillComplianceMetadataService.ParseResult result = service.parseFrontmatter(frontmatter);
assertThat(result.mappings()).isEmpty();
assertThat(result.errors()).containsExactlyInAnyOrder(
"x-astron-compliance[0].evidenceUrl must use an http or https URI",
"x-astron-compliance[1].evidenceUrl must use an http or https URI"
);
}
@Test
void readFromParsedMetadataJson_extractsMappingsFromStoredSkillMetadata() {
String parsedMetadataJson = """

View file

@ -213,6 +213,34 @@ class SkillPackageValidatorTest {
&& error.contains("x-astron-compliance must be a non-empty array")));
}
@Test
void testUnsafeComplianceEvidenceUrlRejected() {
String skillMdContent = """
---
name: compliance-skill
description: Skill with unsafe evidence url
version: 1.0.0
x-astron-compliance:
- standard: gdpr
standardVersion: "2024"
controlId: Article-17
evidenceUrl: "javascript:alert(1)"
---
Body
""";
List<PackageEntry> entries = List.of(
new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown")
);
ValidationResult result = validator.validate(entries);
assertFalse(result.passed());
assertTrue(result.errors().stream().anyMatch(error ->
error.contains("Invalid SKILL.md frontmatter")
&& error.contains("evidenceUrl must use an http or https URI")));
}
@Test
void testPackageTooLarge() {
// Use a custom validator with 2KB total limit to test the logic