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
This commit is contained in:
wowo 2026-03-30 18:13:37 +08:00 • committed by GitHub
parent 1b7bd06685
commit 34de5bb53e
8 changed files with 112 additions and 14 deletions

View file

@ -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).

View file

@ -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 采用清晰的分层架构:

View file

@ -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<String> 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<String> allowedFileExtensions = new LinkedHashSet<>(SkillPackagePolicy.ALLOWED_EXTENSIONS);
public int getMaxFileCount() {
return maxFileCount;

View file

@ -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)

View file

@ -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 {
}
}

View file

@ -22,13 +22,15 @@ public final class SkillPackagePolicy {
public static final Set<String> 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")

View file

@ -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<PackageEntry> 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 = "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"></xs:schema>".getBytes();
List<PackageEntry> 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 = """
---

View file

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