From 2abedc34b4f806581b257944f2bc30f0887c9223 Mon Sep 17 00:00:00 2001 From: kl Date: Tue, 3 Mar 2026 14:35:56 +0800 Subject: [PATCH] fix(security): enforce whitelist with blacklist and harden wildcard rules --- SECURITY_CONFIG.md | 8 +++- .../cn/keking/web/filter/TrustHostFilter.java | 37 ++++++++++++++++++- .../web/filter/TrustHostFilterTests.java | 10 +++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/SECURITY_CONFIG.md b/SECURITY_CONFIG.md index ee87b300..ad96085b 100644 --- a/SECURITY_CONFIG.md +++ b/SECURITY_CONFIG.md @@ -146,11 +146,15 @@ trust.host = * ### Q4: 如何允许子域名? -目前不支持通配符域名匹配,需要明确列出每个子域名: +已支持通配符域名匹配,可使用 `*.example.com`: ```properties -trust.host = cdn.example.com,api.example.com,storage.example.com +trust.host = *.example.com ``` +说明: +- `*.example.com` 会匹配 `cdn.example.com`、`api.internal.example.com`,但不匹配根域 `example.com` +- 对于 IP 风格通配(如 `192.168.*`、`10.*`),仅匹配字面量 IPv4 地址,不匹配域名 + ## 🚨 安全事件响应 如果发现可疑的预览请求: diff --git a/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java b/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java index 478e7936..53441769 100644 --- a/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java +++ b/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java @@ -16,6 +16,7 @@ import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.collections4.CollectionUtils; import org.slf4j.Logger; @@ -51,6 +52,9 @@ public class TrustHostFilter implements Filter { String host = WebUtils.getHost(url); if (isNotTrustHost(host)) { String currentHost = host == null ? "UNKNOWN" : host; + if (response instanceof HttpServletResponse httpServletResponse) { + httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); + } response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setContentType("text/html;charset=UTF-8"); String html = this.notTrustHostHtmlView == null @@ -70,8 +74,9 @@ public class TrustHostFilter implements Filter { } // 如果配置了黑名单,优先检查黑名单 - if (CollectionUtils.isNotEmpty(ConfigConstants.getNotTrustHostSet())) { - return matchAnyPattern(host, ConfigConstants.getNotTrustHostSet()); + if (CollectionUtils.isNotEmpty(ConfigConstants.getNotTrustHostSet()) + && matchAnyPattern(host, ConfigConstants.getNotTrustHostSet())) { + return true; } // 如果配置了白名单,检查是否在白名单中 @@ -121,6 +126,9 @@ public class TrustHostFilter implements Filter { } if (pattern.contains("*")) { + if (isIpv4WildcardPattern(pattern)) { + return matchIpv4Wildcard(host, pattern); + } Pattern compiledPattern = wildcardPatternCache.computeIfAbsent(pattern, key -> Pattern.compile(wildcardToRegex(key))); return compiledPattern.matcher(host).matches(); } @@ -128,6 +136,31 @@ public class TrustHostFilter implements Filter { return host.equals(pattern); } + private boolean isIpv4WildcardPattern(String pattern) { + return pattern.matches("^[0-9.*]+$") && pattern.contains("."); + } + + private boolean matchIpv4Wildcard(String host, String pattern) { + if (parseLiteralIpv4(host) == null) { + return false; + } + String[] hostParts = host.split("\\."); + String[] patternParts = pattern.split("\\."); + if (hostParts.length != 4 || patternParts.length < 1 || patternParts.length > 4) { + return false; + } + for (int i = 0; i < patternParts.length; i++) { + String p = patternParts[i]; + if ("*".equals(p)) { + continue; + } + if (!p.equals(hostParts[i])) { + return false; + } + } + return true; + } + private String wildcardToRegex(String wildcard) { StringBuilder regexBuilder = new StringBuilder("^"); String[] parts = wildcard.split("\\*", -1); diff --git a/server/src/test/java/cn/keking/web/filter/TrustHostFilterTests.java b/server/src/test/java/cn/keking/web/filter/TrustHostFilterTests.java index e69c3593..4ae8029b 100644 --- a/server/src/test/java/cn/keking/web/filter/TrustHostFilterTests.java +++ b/server/src/test/java/cn/keking/web/filter/TrustHostFilterTests.java @@ -21,6 +21,7 @@ public class TrustHostFilterTests { assert trustHostFilter.isNotTrustHost("192.168.1.10"); assert !trustHostFilter.isNotTrustHost("8.8.8.8"); + assert !trustHostFilter.isNotTrustHost("192.168.evil.com"); } @Test @@ -79,4 +80,13 @@ public class TrustHostFilterTests { assert trustHostFilter.isNotTrustHost("10.1.2.3"); assert !trustHostFilter.isNotTrustHost("8.8.8.8"); } + + @Test + void shouldStillEnforceWhitelistWhenBlacklistConfigured() { + ConfigConstants.setTrustHostValue("internal.example.com"); + ConfigConstants.setNotTrustHostValue("127.0.0.1"); + + assert !trustHostFilter.isNotTrustHost("internal.example.com"); + assert trustHostFilter.isNotTrustHost("8.8.8.8"); + } }