kkFileView 安全配置实战:trust.host 白名单、黑名单与 TrustHostFilter 防 SSRF 机制详解
【免费下载链接】kkFileViewUniversal File Online Preview Project based on Spring-Boot项目地址: https://gitcode.com/GitHub_Trending/kk/kkFileView
本篇指南基于 kkFileView 仓库根目录的 SECURITY_CONFIG.md 展开,系统讲解 4.4.0 之后版本默认拒绝外部文件源、trust.host白名单与not.trust.host黑名单的完整配置方式、Docker 环境下的环境变量注入、配置验证方法与安全事件响应流程,并结合 TrustHostFilter.java 等源码深入剖析主机匹配算法(精确、通配符、IPv4 通配、CIDR)与黑名单优先于白名单的判定链路,帮助你把 kkFileView 的 SSRF(服务器端请求伪造)防护真正落到生产环境。
一、为什么要做主机信任校验:4.4.0 之后的默认拒绝策略
kkFileView 是一个基于 Spring Boot 的通用文件在线预览项目。它的核心入口onlinePreview、getCorsFile、addTask等接口都接收一个“文件源 URL”参数,由服务端去拉取并转换该 URL 指向的文件。这类“服务端替你请求任意 URL”的设计天然存在 SSRF 风险:攻击者可以构造http://127.0.0.1:8080/admin、云厂商元数据地址http://169.254.169.254/...之类的 URL,诱导预览服务去探测内网。
因此,从 4.4.0 之后版本开始,kkFileView 增强了安全性:默认拒绝所有未配置的外部文件预览请求。这一点在 application.properties 的“安全与访问控制配置”小节中也有明确注释:
# 信任站点白名单配置,多个用','隔开 # ⚠️ 安全提示:为防止SSRF攻击,强烈建议配置信任主机白名单 # ⚠️ 如果不配置,系统将默认拒绝所有外部文件预览请求 # 配置示例: # trust.host = kkview.cn,yourdomain.com,cdn.example.com # 如果需要允许所有域名(不推荐,仅用于测试环境),请设置为: # trust.host = * # 当前配置:默认本机测试 (正式启用请修改) trust.host = ${KK_TRUST_HOST:default} # 不信任站点黑名单配置,多个用逗号隔开 # 黑名单优先级高于白名单,设置后将禁止预览来自这些站点的文件 # 建议配置:禁止访问内网地址和本地地址,防止内部信息泄露 not.trust.host = ${KK_NOT_TRUST_HOST:default}注意default占位值:在 ConfigConstants.java 中,当trust.host取值为default(未配置)时,对应的主机集合会被初始化为空集合,而不是“放行所有”。这个“空集合 + 默认拒绝”的组合就是 4.4.0 行为变化的实现基础,也是很多团队升级后“预览突然全部失效”的根因。
1.1 校验发生在哪些接口
在 WebConfig.java 中,TrustHostFilter通过FilterRegistrationBean注册,只拦截以下四个预览入口:
Set<String> filterUri = new HashSet<>(); filterUri.add("/onlinePreview"); filterUri.add("/picturesPreview"); filterUri.add("/getCorsFile"); filterUri.add("/addTask");也就是说,任何最终指向外部文件源的请求——无论是页面预览(onlinePreview)、图片预览(picturesPreview)、跨域取文件(getCorsFile)还是大文件异步转换任务(addTask)——在进入 Controller 之前都会先经过主机信任校验。
1.2 源 URL 是怎么被取出来的
WebUtils.getSourceUrl() 按优先级从请求参数url、currentUrl、urlPath、urls中提取文件源地址,并支持 Base64 / AES 解码(decodeUrl,见 WebUtils.java)。这意味着即使攻击者把恶意 URL 做 Base64 编码绕开前端审查,TrustHostFilter仍然能拿到解码后的真实地址再做判定——校验发生在解码之后、转换之前,这是该防护链路能够成立的第一个关键点。
二、信任主机白名单配置(trust.host)
2.1 两种配置方式
方式 1:通过配置文件。在 application.properties 中配置允许预览的域名,多个域名用英文逗号分隔:
trust.host = kkview.cn,yourdomain.com,cdn.example.com方式 2:通过环境变量。适合 Docker / K8s 等不便改配置文件的部署形态:
KK_TRUST_HOST=kkview.cn,yourdomain.com,cdn.example.com两者等价,因为属性值本身就是trust.host = ${KK_TRUST_HOST:default},环境变量会覆盖配置文件默认值。
示例场景:只允许预览来自oss.aliyuncs.com和cdn.example.com的文件:
trust.host = oss.aliyuncs.com,cdn.example.com2.2 配置的解析细节(源码佐证)
从 ConfigConstants.setTrustHostValue() 的实现看,白名单字符串在入库前有三次规范化处理:
private static CopyOnWriteArraySet<String> getHostValue(String trustHost) { return DEFAULT_VALUE.equalsIgnoreCase(trustHost) ? new CopyOnWriteArraySet<>() : new CopyOnWriteArraySet<>(Arrays.asList(trustHost.toLowerCase().replaceAll("\\s+", "").split(","))); }- 小写化:
trustHost.toLowerCase(),配合TrustHostFilter中对请求主机同样toLowerCase的处理,域名大小写实际不影响匹配结果; - 去空白:
replaceAll("\\s+", ""),"a.com, b.com"与"a.com,b.com"等价; - 按逗号切分存入
CopyOnWriteArraySet,该集合类型保证了动态刷新时读线程不受写影响。
此外,ConfigRefreshComponent.java 会周期性(间隔由kk.refreshschedule控制,application.properties 中默认为 2 秒)重新读取trust.host与not.trust.host并调用对应的setXxxValue热更新内存值。结论:白名单/黑名单修改后无需重启服务即可生效,这对线上应急加黑名单尤其重要。
三、允许所有主机:trust.host = *(仅测试环境)
trust.host = *⚠️警告:此配置会允许访问任意外部地址,存在安全风险,仅应在测试环境使用!
源码层面,*在 TrustHostFilter.isNotTrustHost() 中是白名单的“超级开关”:
// 支持通配符 * 表示允许所有主机 if (ConfigConstants.getTrustHostSet().contains("*")) { logger.debug("允许所有主机访问(通配符模式): {}", host); return false; }但要注意:即使白名单是*,黑名单依然先生效(见下文第四节)。因此trust.host = *+not.trust.host = 127.0.0.1,192.168.*是比裸*安全的折中写法,测试环境也建议保留内网黑名单。
四、黑名单配置(not.trust.host,高级)
禁止特定域名或内网地址:
# 禁止访问内网地址(强烈推荐) not.trust.host = localhost,127.0.0.1,192.168.*,10.*,172.16.*,169.254.* # 禁止特定恶意域名 not.trust.host = malicious-site.com,spam-domain.net优先级:黑名单 > 白名单。这一优先级不是约定俗成,而是判定顺序决定的——isNotTrustHost()先查黑名单、后查白名单:
// 如果配置了黑名单,优先检查黑名单 if (CollectionUtils.isNotEmpty(ConfigConstants.getNotTrustHostSet()) && matchAnyPattern(host, ConfigConstants.getNotTrustHostSet())) { return true; // 命中黑名单,直接拒绝 } // 如果配置了白名单,检查是否在白名单中 if (CollectionUtils.isNotEmpty(ConfigConstants.getTrustHostSet())) { if (ConfigConstants.getTrustHostSet().contains("*")) { return false; } return !matchAnyPattern(host, ConfigConstants.getTrustHostSet()); } // 安全加固:默认拒绝所有未配置的主机(防止SSRF攻击) logger.warn("未配置信任主机列表,拒绝访问主机: {},请在配置文件中设置 trust.host 或 KK_TRUST_HOST 环境变量", host); return true;TrustHostFilterTests.java 中的shouldKeepBlacklistHigherPriorityThanWhitelist用例对该行为做了回归验证:白名单为*时,127.0.0.1、10.1.2.3仍被黑名单拦截,而8.8.8.8放行。
4.1 四种匹配模式(源码级解析)
matchHostPattern() 支持四种匹配方式,白名单和黑名单通用:
| 模式 | 示例 | 说明 |
|---|---|---|
| 精确匹配 | example.com | 与主机小写形式完全相等 |
| 全局通配 | * | 仅白名单有意义,放行一切(黑名单仍优先生效) |
| 域名通配 | *.example.com | 编译为正则,*.example.com匹配cdn.example.com、api.internal.example.com,不匹配根域example.com |
| IPv4 通配 | 192.168.* | 仅对字面量 IPv4地址逐段匹配,不匹配任何域名(如192.168.evil.com) |
| IPv4 CIDR | 192.168.0.0/16 | 按网络掩码位运算判断,支持 /0–/32 |
其中两处实现细节值得注意:
(1)IPv4 通配只认字面量 IP。isIpv4WildcardPattern()要求模式形如^[0-9.*]+$,matchIpv4Wildcard()还强制要求被匹配的主机本身是合法的 4 段 IPv4 字面量。配合 TrustHostFilterTests.shouldBlockWildcardNotTrustHostPattern 的用例:192.168.*拦截192.168.1.10,但不拦截域名形式的192.168.evil.com——它会被当作普通域名走白名单逻辑,而不是被 IP 规则误伤。
(2)CIDR 匹配刻意不做 DNS 解析。parseLiteralIpv4() 的注释写得很直白:
/** * 仅解析字面量 IPv4 地址(不做 DNS 解析),防止 DNS rebinding/TOCTOU 风险。 */如果实现中先做 DNS 解析再判 CIDR,攻击者可以用一个“检查时解析为公网 IP、请求时解析为内网 IP”的域名实施 DNS rebinding 绕过。只认字面量 IP 就从根上排除了这条攻击路径;对应的测试shouldBlockCidrNotTrustHostPattern也断言了localhost这类域名不会被 CIDR 规则匹配(因为它不是字面量 IP)。测试集还覆盖了高位字节(200.0.0.0/8)与上界255.255.255.255/32的位运算正确性。
(3)通配符编译带缓存。域名通配会经wildcardToRegex()编译成正则并缓存进wildcardPatternCache(ConcurrentHashMap),高频请求下不会重复编译;Pattern.quote保证了字面量片段不被解释为正则元字符。
五、拒绝响应的完整链路
理解“配置如何变成一次 403”,需要把三段代码串起来(TrustHostFilter.doFilter()):
- 取址:
WebUtils.getSourceUrl(request)从url/currentUrl/urlPath/urls参数取文件源(Base64/AES 解码后),WebUtils.getHost(url)(WebUtils.java)解析出小写主机; - 判定:
isNotTrustHost(host) || !WebUtils.isValidUrl(url)任一为真即拒绝。isValidUrl只放行http/https/ftp/rtsp/mms/file六种协议头(WebUtils.java),gopher://、jar://这类常用于 SSRF 探测的协议直接出局;主机为null(如 file 协议、URL 解析失败)时isNotTrustHost也返回 true; - 响应:HTTP 状态码置为
403 FORBIDDEN,输出类路径下web/notTrustHost.html页面(init()时预读入内存),并把${current_host}占位符替换为实际主机名,让用户看到“当前预览文件来自不受信任的站点:xxx”。
这套“协议白名单 + 主机信任 + 明确 403 页面”的组合,与配置层的prohibit(禁止exe,dll,dat等文件类型)一起构成了 kkFileView 预览侧的纵深防御。
六、Docker 环境配置
在容器化部署中通过-e注入环境变量即可,无需改镜像内配置:
docker run -d \ -e KK_TRUST_HOST=yourdomain.com,cdn.example.com \ -e KK_NOT_TRUST_HOST=localhost,127.0.0.1 \ -p 8012:8012 \ keking/kkfileview:4.4.0该方式与 K8s 完全兼容(env段写入同名变量即可)。由于trust.host/not.trust.host支持ConfigRefreshComponent热刷新,通过挂载配置卷修改 properties 后也无需重启容器(默认 2 秒周期内生效),这为滚动升级期间的临时策略调整提供了便利。仓库中同时提供 Dockerfile 与 docker/kkfileview-base 基础镜像,可在其基础上做二次构建。
七、生产环境推荐配置与反例
7.1 推荐配置
# 1. 明确配置信任主机白名单 trust.host = your-cdn.com,your-storage.com # 2. 配置黑名单防止内网访问 not.trust.host = localhost,127.0.0.1,192.168.*,10.*,172.16.* # 3. 禁用文件上传(生产环境) file.upload.disable = true # 4. 配置基础URL(使用反向代理时) base.url = https://preview.yourdomain.com补充两点与上述配置相关的说明:
file.upload.disable = true在 application.properties 中当前默认值即为 true(“十一、首页与文件管理配置”小节),生产环境保持默认即可;base.url在使用 Nginx 等反向代理时必须显式配置为对外服务地址,否则预览页面拼出的资源地址会指向代理内部地址而无法加载。
7.2 不推荐配置
# 危险:允许所有主机访问 trust.host = * # 危险:启用文件上传(生产环境) file.upload.disable = false另外两个常被忽略的相关开关也建议在生产环境保持收紧状态(application.properties 当前默认值):kk.ignore.ssl = false(启用完整证书验证,避免中间人降级风险)与kk.enable.redirect = false(禁用 URL 重定向跟随,防止借 302 跳转到未受控主机绕过白名单直觉)。
八、配置验证:确认白名单/黑名单真的生效
8.1 测试白名单是否生效
- 配置白名单:
trust.host = kkview.cn- 尝试预览白名单内的文件(注意:实际接口中 url 参数为 Base64 编码后的值,此处为语义示意):
http://localhost:8012/onlinePreview?url=https://kkview.cn/test.pdf ✅ 应该可以正常预览- 尝试预览白名单外的文件:
http://localhost:8012/onlinePreview?url=https://other-domain.com/test.pdf ❌ 应该被拒绝,返回 403 并显示“不信任的文件源”页面(notTrustHost.html)8.2 测试黑名单是否生效
- 配置黑名单:
not.trust.host = localhost,127.0.0.1- 尝试访问本地文件:
http://localhost:8012/getCorsFile?urlPath=http://127.0.0.1:8080/admin ❌ 应该被拒绝8.3 用单元测试做本地回归
如果要在本地确认匹配引擎行为而不是起服务,可直接运行 TrustHostFilterTests.java(8 个用例覆盖:IPv4 通配、CIDR、高位/上界 CIDR、空白主机拒绝、白名单通配、黑名单优先级、黑名单存在时白名单仍强制)。测试通过ConfigConstants.setTrustHostValue(...)直接注入内存配置,@AfterEach统一还原为default,互不污染——这也是官方验证“配置语义”的权威依据。
九、常见问题(FAQ)
Q1:升级后无法预览文件了?
原因:新版本默认拒绝未配置的主机(即trust.host仍为default时主机集合为空,全部请求落入“默认拒绝”分支)。
解决:在配置文件中添加信任主机列表:
trust.host = your-file-server.comQ2:如何临时恢复旧版本行为?
不推荐,但如果确实需要:
trust.host = *Q3:配置了白名单但还是无法访问?
排查清单:
- 域名是否完全匹配。需要说明的是,从源码看(
getHostValue与isNotTrustHost两侧均做小写归一化),域名大小写差异不会导致匹配失败;若仍失败,重点检查拼写、多余空格(空格会被自动去除,一般无影响)以及是否写成了带协议的完整 URL 而非纯主机名; - 是否配置了黑名单——黑名单优先级更高,可能命中了意料之外的条目;
- 查看日志中的 WARNING 信息:默认拒绝分支会打印“未配置信任主机列表,拒绝访问主机: xxx”,主机为空时会打印“主机名为空或无效,拒绝访问”,这两条日志能快速定位是“没配白名单”还是“URL 解析失败”;
- 确认环境变量(
KK_TRUST_HOST/KK_NOT_TRUST_HOST)是否设置正确——注意环境变量会覆盖配置文件中的属性占位默认值; - 确认改动是否已完成热刷新(默认
kk.refreshschedule = 2秒),必要时查看刷新组件日志。
Q4:如何允许子域名?
已支持通配符域名匹配,可使用*.example.com:
trust.host = *.example.com说明:
*.example.com会匹配cdn.example.com、api.internal.example.com,但不匹配根域example.com——需要根域时请显式追加example.com;- 对于 IP 风格通配(如
192.168.*、10.*),仅匹配字面量 IPv4 地址,不匹配域名(对应isIpv4WildcardPattern+matchIpv4Wildcard实现)。
此外,若内网段较复杂,黑名单可直接使用 CIDR 写法(如192.168.0.0/16、172.16.0.0/12),源码已支持且同样不做 DNS 解析。
十、安全事件响应
如果发现可疑的预览请求,按以下流程处置:
- 查日志:搜索 “拒绝访问主机” 关键字(默认拒绝分支)与 “主机名为空或无效,拒绝访问” 关键字,统计被拦截主机的分布;
- 核对白名单:确认
trust.host配置是否合理,是否存在过宽的*或宽泛通配; - 查网络面:检查是否有异常的外发网络请求(结合代理/防火墙日志),重点关注对
169.254.*元数据地址与内网管理端口的探测; - 动态加黑名单:利用配置热刷新能力,将可疑域名/网段追加到
not.trust.host(支持通配与 CIDR),无需重启即生效; - 上报:若怀疑存在通用漏洞(与个人配置无关),按 SECURITY.md 的安全策略,通过项目的私密漏洞报告渠道提交受影响版本、部署方式、复现步骤与脱敏日志,不要在公开渠道披露细节。
十一、小结:最小权限原则落地清单
| 配置项 | 属性 / 环境变量 | 生产建议 | 默认值(当前仓库) |
|---|---|---|---|
| 信任白名单 | trust.host/KK_TRUST_HOST | 明确列出文件源域名,不用* | default(= 全部拒绝) |
| 不信任黑名单 | not.trust.host/KK_NOT_TRUST_HOST | 至少覆盖localhost,127.0.0.1,192.168.*,10.*,172.16.*,169.254.* | default(= 空) |
| 文件上传开关 | file.upload.disable | true | true |
| 反向代理地址 | base.url | 代理部署时必填 | default |
| SSL 验证 | kk.ignore.ssl | false(生产收紧) | false |
| 重定向跟随 | kk.enable.redirect | false | false |
kkFileView 的这套 SSRF 防护可以概括为三层:入口层协议白名单(isValidUrl)→判定层黑名单优先 + 白名单 + 默认拒绝(TrustHostFilter,含 CIDR/通配匹配且不解析 DNS)→响应层403 + 明确的拒绝页面。配置侧只需维护trust.host与not.trust.host两个逗号分隔列表(支持热刷新),即可在“可用性”与“安全性”之间获得可控的平衡点。遵循最小权限原则、定期复核信任主机列表,是保持这套机制长期有效的基本要求。
【免费下载链接】kkFileViewUniversal File Online Preview Project based on Spring-Boot项目地址: https://gitcode.com/GitHub_Trending/kk/kkFileView
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考