From 34de5bb53e85f1dfe9b191f0b4caf2db87119d90 Mon Sep 17 00:00:00 2001 From: wowo Date: Mon, 30 Mar 2026 18:13:37 +0800 Subject: [PATCH] feat(upload): allow xsd, xsl, dtd file types in skill packages (#185) * feat(upload): allow xsd, xsl, dtd file types in skill packages Add XML schema-related file extensions (.xsd, .xsl, .dtd) to the upload allowlist and text content validation. Users uploading skills with XML Schema files (e.g., Anthropic's docx skill) were getting rejected because .xsd was not in the allowed extensions list. Closes #165 * feat(upload): expand office file allowlist * test(app): verify publish extension env override * docs(readme): document upload allowlist override --- README.md | 17 ++++++++ README_zh.md | 17 ++++++++ .../config/SkillPublishProperties.java | 9 +---- .../src/main/resources/application.yml | 3 +- .../config/SkillPublishPropertiesTest.java | 39 +++++++++++++++++++ .../skill/validation/SkillPackagePolicy.java | 11 ++++-- .../validation/SkillPackageValidatorTest.java | 23 +++++++++++ web/src/features/skill/file-type-utils.ts | 7 +++- 8 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/config/SkillPublishPropertiesTest.java diff --git a/README.md b/README.md index 394fb577..8971aec7 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,23 @@ Recommended production baseline: If the GHCR package remains private, run `docker login ghcr.io` before `docker compose up -d`. +### Upload Allowlist Override + +Skill package upload validation uses the default extension allowlist from +[`SkillPackagePolicy.java`](./server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackagePolicy.java). +`SkillPublishProperties` uses that same list by default for +`skillhub.publish.allowed-file-extensions`. + +If you need to replace the default allowlist at runtime, set: + +```bash +SKILLHUB_PUBLISH_ALLOWED_FILE_EXTENSIONS=.md,.json,.xsd,.xsl,.dtd,.docx,.xlsx,.pptx +``` + +Spring Boot binds this environment variable to +`skillhub.publish.allowed-file-extensions`. When set, it replaces the default +allowlist instead of appending to it. + ### Monitoring A Prometheus + Grafana monitoring stack lives under [`monitoring/`](./monitoring). diff --git a/README_zh.md b/README_zh.md index 534142cc..0bdaaf3f 100644 --- a/README_zh.md +++ b/README_zh.md @@ -204,6 +204,23 @@ AUTH_SESSION_TIMEOUT=30m 完整配置参考请查看 [`application.yml`](./server/skillhub-app/src/main/resources/application.yml)。 +### 上传白名单覆盖 + +技能包上传校验默认使用 +[`SkillPackagePolicy.java`](./server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackagePolicy.java) +中的扩展名白名单。`SkillPublishProperties` 默认也会把这份列表作为 +`skillhub.publish.allowed-file-extensions` 的值。 + +如果需要在运行时整体替换默认白名单,可以设置: + +```bash +SKILLHUB_PUBLISH_ALLOWED_FILE_EXTENSIONS=.md,.json,.xsd,.xsl,.dtd,.docx,.xlsx,.pptx +``` + +Spring Boot 会把这个环境变量绑定到 +`skillhub.publish.allowed-file-extensions`。一旦设置,该配置会替换默认白名单, +而不是在默认列表后追加。 + ## 架构 SkillHub 采用清晰的分层架构: diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillPublishProperties.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillPublishProperties.java index 98cb6de7..f316ea37 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillPublishProperties.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillPublishProperties.java @@ -1,5 +1,6 @@ package com.iflytek.skillhub.config; +import com.iflytek.skillhub.domain.skill.validation.SkillPackagePolicy; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -13,13 +14,7 @@ public class SkillPublishProperties { private int maxFileCount = 100; private long maxSingleFileSize = 10 * 1024 * 1024; // 10MB private long maxPackageSize = 100 * 1024 * 1024; - private Set allowedFileExtensions = new LinkedHashSet<>(Set.of( - ".md", ".txt", ".json", ".yaml", ".yml", ".html", ".css", ".csv", ".pdf", - ".toml", ".xml", ".ini", ".cfg", ".env", - ".js", ".ts", ".py", ".sh", ".rb", ".go", ".rs", ".java", ".kt", - ".lua", ".sql", ".r", ".bat", ".ps1", ".zsh", ".bash", - ".png", ".jpg", ".jpeg", ".svg", ".gif", ".webp", ".ico" - )); + private Set allowedFileExtensions = new LinkedHashSet<>(SkillPackagePolicy.ALLOWED_EXTENSIONS); public int getMaxFileCount() { return maxFileCount; diff --git a/server/skillhub-app/src/main/resources/application.yml b/server/skillhub-app/src/main/resources/application.yml index 3ab88d28..8a3872af 100644 --- a/server/skillhub-app/src/main/resources/application.yml +++ b/server/skillhub-app/src/main/resources/application.yml @@ -112,7 +112,8 @@ skillhub: max-file-count: 100 max-single-file-size: 10485760 # 10MB max-package-size: 104857600 # 100MB - allowed-file-extensions: .md,.txt,.json,.yaml,.yml,.html,.css,.csv,.pdf,.toml,.xml,.ini,.cfg,.env,.js,.ts,.py,.pyc,.sh,.rb,.go,.rs,.java,.kt,.lua,.sql,.r,.bat,.ps1,.zsh,.bash,.png,.jpg,.jpeg,.svg,.gif,.webp,.ico + # allowed-file-extensions: uses SkillPackagePolicy.ALLOWED_EXTENSIONS by default + # Override via SKILLHUB_PUBLISH_ALLOWED_FILE_EXTENSIONS env var if needed profile: moderation: machine-review: ${SKILLHUB_PROFILE_MACHINE_REVIEW_ENABLED:true} # Enable machine review (e.g. sensitive word detection) diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/config/SkillPublishPropertiesTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/config/SkillPublishPropertiesTest.java new file mode 100644 index 00000000..6bda9a44 --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/config/SkillPublishPropertiesTest.java @@ -0,0 +1,39 @@ +package com.iflytek.skillhub.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.SystemEnvironmentPropertySource; + +import java.util.Map; + +class SkillPublishPropertiesTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(TestConfig.class); + + @Test + void bindsAllowedFileExtensionsFromEnvironmentStyleProperty() { + contextRunner + .withInitializer((context) -> context.getEnvironment().getPropertySources().addFirst( + new SystemEnvironmentPropertySource( + "test-env", + Map.of("SKILLHUB_PUBLISH_ALLOWED_FILE_EXTENSIONS", ".docx,.xsd,.pptx") + ) + )) + .run((context) -> { + SkillPublishProperties properties = context.getBean(SkillPublishProperties.class); + + assertThat(properties.getAllowedFileExtensions()) + .containsExactly(".docx", ".xsd", ".pptx"); + }); + } + + @Configuration + @EnableConfigurationProperties(SkillPublishProperties.class) + static class TestConfig { + } +} diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackagePolicy.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackagePolicy.java index 87a952b9..49a5f052 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackagePolicy.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackagePolicy.java @@ -22,13 +22,15 @@ public final class SkillPackagePolicy { public static final Set ALLOWED_EXTENSIONS = Set.of( // Documentation ".md", ".txt", ".json", ".yaml", ".yml", ".html", ".css", ".csv", ".pdf", - // Configuration - ".toml", ".xml", ".ini", ".cfg", ".env", + // Configuration and schemas + ".toml", ".xml", ".xsd", ".xsl", ".dtd", ".ini", ".cfg", ".env", // Scripts and source code ".js", ".ts", ".py", ".sh", ".rb", ".go", ".rs", ".java", ".kt", ".lua", ".sql", ".r", ".bat", ".ps1", ".zsh", ".bash", // Images - ".png", ".jpg", ".jpeg", ".svg", ".gif", ".webp", ".ico" + ".png", ".jpg", ".jpeg", ".svg", ".gif", ".webp", ".ico", + // Office documents + ".docx", ".xlsx", ".pptx" ); private SkillPackagePolicy() { @@ -126,7 +128,8 @@ public final class SkillPackagePolicy { || path.endsWith(".json") || path.endsWith(".yaml") || path.endsWith(".yml") || path.endsWith(".js") || path.endsWith(".ts") || path.endsWith(".py") || path.endsWith(".sh") || path.endsWith(".html") || path.endsWith(".css") || path.endsWith(".csv") - || path.endsWith(".toml") || path.endsWith(".xml") || path.endsWith(".ini") + || path.endsWith(".toml") || path.endsWith(".xml") || path.endsWith(".xsd") + || path.endsWith(".xsl") || path.endsWith(".dtd") || path.endsWith(".ini") || path.endsWith(".cfg") || path.endsWith(".env") || path.endsWith(".rb") || path.endsWith(".go") || path.endsWith(".rs") || path.endsWith(".java") || path.endsWith(".kt") || path.endsWith(".lua") diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidatorTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidatorTest.java index 44c57254..39de4640 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidatorTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidatorTest.java @@ -286,6 +286,29 @@ class SkillPackageValidatorTest { assertTrue(result.passed()); } + @Test + void acceptsDocxFile() { + // DOCX is a ZIP-based format; PK magic bytes (0x50, 0x4b, 0x03, 0x04) + byte[] docxContent = new byte[] {0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00}; + List entries = List.of( + skillMdEntry(), + new PackageEntry("document.docx", docxContent, docxContent.length, "application/vnd.openxmlformats-officedocument.wordprocessingml.document") + ); + ValidationResult result = validator.validate(entries); + assertTrue(result.passed()); + } + + @Test + void acceptsXsdSchemaFile() { + byte[] xsdContent = "".getBytes(); + List entries = List.of( + skillMdEntry(), + new PackageEntry("schema.xsd", xsdContent, xsdContent.length, "application/xml") + ); + ValidationResult result = validator.validate(entries); + assertTrue(result.passed()); + } + private PackageEntry skillMdEntry() { String skillMdContent = """ --- diff --git a/web/src/features/skill/file-type-utils.ts b/web/src/features/skill/file-type-utils.ts index ebc4ed64..dd818470 100644 --- a/web/src/features/skill/file-type-utils.ts +++ b/web/src/features/skill/file-type-utils.ts @@ -14,8 +14,8 @@ const PREVIEWABLE_EXTENSIONS = new Set([ 'ts', 'tsx', 'js', 'jsx', 'json', 'yaml', 'yml', 'py', 'java', 'go', 'rs', 'c', 'cpp', 'h', 'hpp', 'sh', 'bash', 'zsh', 'fish', - // Config - 'txt', 'xml', 'toml', 'ini', 'env', + // Config and schemas + 'txt', 'xml', 'xsd', 'xsl', 'dtd', 'toml', 'ini', 'env', // Web 'html', 'css', 'scss', 'sass', 'less', 'vue', 'svelte', @@ -178,6 +178,9 @@ export function getLanguageForHighlight(fileName: string): string | null { yml: 'yaml', toml: 'toml', xml: 'xml', + xsd: 'xml', + xsl: 'xml', + dtd: 'xml', ini: 'ini', // Web files