背景:一个 9.8 分的漏洞通报
某天看到一份漏洞通报(QVD-2026-57410):DeepSeek Harness(DSH)的 /api 接口用 HTTP Host 请求头判断请求是否来自本机回环,以此把一批高权限 RPC 接口(bash、文件读写、代码执行)限定在本地访问。
问题在于:Host 头是客户端可以任意构造的字段。如果 DSH 被部署到公网或经反向代理对外暴露,攻击者只需伪造 Host: 127.0.0.1,就能让高权限接口把远程请求当成”本机请求”放行,进而注册虚假 LLM Provider、驱动 Agent 工具执行任意系统命令——整条链路不需要任何有效 API Key,CVSS 9.8。
我正好在一台云服务器上远程使用 DSH,于是做了一次完整的排查和加固。这篇是全过程实录,文末有可复用的脚本。
第一步:摸清自己的真实暴露面
漏洞公告说的是”受影响版本 X”,但安全排查不能只看版本号,要看实际部署形态。三个关键事实:
# 1. 安装的版本
cat /path/to/dsh/package.json | grep version
# 2. 监听地址——决定外部能否直达
ss -tlnp | grep 3080
# 3. 进程启动参数
ps aux | grep dsh
我的情况:
- DSH 监听
127.0.0.1:3080,公网不可直达 ✅ - 但存在一条远程访问链:
公网 → nginx:8080 → 认证代理:3081 → dsh:3080 - 启动参数里有
--trusted-host——这正是”信任 Host 头”的模式,通报的根因在这个版本里同样存在
也就是说:漏洞代码模式存在,但能否被利用,取决于前置链路是否先做了认证。
第二步:模拟攻击者,逐跳验证
不猜,直接打。对每一跳分别测试”未认证 + 伪造 Host”会发生什么:
# 1. 经 nginx 入口,不带任何凭证
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
# → 303(跳转登录页)✅
# 2. 经 nginx,伪造 Host 头打 RPC
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H 'Host: 127.0.0.1:3080' -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"bash.exec","params":{"command":"id"}}' \
http://127.0.0.1:8080/api/rpc
# → 401 ✅ 认证在 Host 改写之前拦截
# 3. WebSocket 通道同样未认证
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Connection: upgrade' -H 'Upgrade: websocket' \
http://127.0.0.1:8080/api/events.mux
# → 401 ✅
# 4. 本机直连 3080 伪造 Host(验证漏洞模式确实存在)
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: 127.0.0.1' http://127.0.0.1:3080/api/xxx
# → 404 而非 403:Host 信任判断生效了,伪造被接受 ⚠️
结论:远程攻击链在第二步就被 401 斩断——认证代理先做认证、通过后才把 Host/Origin 改写为 loopback 转发。这恰好命中通报给出的缓解措施”反向代理层补一道独立认证”。
但第 4 条测试说明:那层”Host 头信任”的窗户纸真实存在,任何能在本机回环上发请求的进程都能捅破它。
第三步:真正的风险在横向移动
盘点这台机器上其他暴露在 0.0.0.0 的服务——其中任何一个被攻破或有 SSRF,都能成为打 127.0.0.1:3080 的跳板:
ss -tlnp | grep 0.0.0.0
docker ps --format '{{.Names}}\t{{.Ports}}'
结果不容乐观:
| 端口 | 服务 | 状态 |
|---|---|---|
| 5432 | PostgreSQL(rag 库) | 公网裸奔,日志里正被爆破(pgg_superadmins、wog 之类的扫描器用户名) |
| 18154 | PostgreSQL(业务库) | 公网暴露 |
| 3000 | PostgREST API | 公网裸奔 |
| 5244/5245 | Alist 网盘 | 公网裸奔 |
| 8081 | 静态博客 | 公网(静态文件,几乎无攻击面) |
而防火墙现状:ufw 未启用,iptables 只有一个”封扫描器 IP”的黑名单链——追着坏人跑的被动防御,默认全放。
第四步:加固实施
4.1 Docker 端口白名单(关键认知:docker 不走 INPUT 链)
Docker 发布端口走 DNAT/FORWARD,ufw 和 INPUT 链的 DROP 根本碰不到它,必须在 DOCKER-USER 链上下手:
# 已建立连接、本机回环、docker 内网放行
iptables -I DOCKER-USER 1 -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
iptables -I DOCKER-USER 2 -s 127.0.0.0/8 -j RETURN
iptables -I DOCKER-USER 3 -s 172.16.0.0/12 -j RETURN # 容器间互访不受影响
# 其余来源访问 3000/5244/5245 一律丢包
iptables -A DOCKER-USER -p tcp -m multiport --dports 3000,5244,5245 -j DROP
验证(经公网 IP 访问,源地址非回环,模拟外部):
curl -m 5 http://<server-ip>:3000/ # 超时,DROP 计数器增长 ✅
curl -m 5 http://127.0.0.1:3000/ # 200 ✅
4.2 规则持久化:重启不丢
iptables 规则重启即失,且 docker 重建链时也会冲掉 DOCKER-USER 的内容。写了一个幂等脚本 + systemd oneshot 单元(After=docker.service),开机自动重放全部规则。要点是每条规则先 -C 检查再 -I/-A 插入,保证幂等可重复执行。
4.3 fail2ban 覆盖 PostgreSQL(踩了两个坑)
fail2ban 原本只护了 SSH。给数据库加爆破防护需要解决两个问题:
坑 1:docker 日志里捞不出 IP。 一个库的 log_line_prefix 不含 %h,认证失败日志没有客户端 IP。解决:
ALTER SYSTEM SET log_line_prefix='%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h ';
SELECT pg_reload_conf(); -- 无需重启
坑 2:fail2ban 正则的两个反直觉行为。
datepattern匹配成功后,failregex 作用在剥离时间戳的行上,^锚点会失效——别用^开头- 针对 docker json 日志,指定
datepattern = "log":"%Y-%m-%d %H:%M:%S可绕开一个 datedetector 的 IndexError bug
最终的 filter / action / jail:
# filter.d/postgresql-docker.conf
[Definition]
datepattern = "log":"%%Y-%%m-%%d %%H:%%M:%%S
failregex = client=<ADDR>[^"]*FATAL:\s+(password authentication failed|unsupported frontend protocol|no pg_hba\.conf entry)
# action.d/iptables-docker-user.conf —— 封禁必须进 DOCKER-USER,进 INPUT 没用
[Definition]
actionstart = iptables -w -N f2b-postgres
iptables -w -C DOCKER-USER -j f2b-postgres || iptables -w -I DOCKER-USER 1 -j f2b-postgres
actionban = iptables -w -I f2b-postgres 1 -s <ip> -j DROP
actionunban = iptables -w -D f2b-postgres -s <ip> -j DROP
actionstop = iptables -w -D DOCKER-USER -j f2b-postgres
iptables -w -F f2b-postgres
iptables -w -X f2b-postgres
# jail.d/postgresql-docker.conf
[postgresql-docker]
enabled = true
filter = postgresql-docker
backend = polling # glob 路径用 polling 才可靠
logpath = /var/lib/docker/containers/*/*-json.log # 通配,容器重建不换路径
maxretry = 10
findtime = 600
bantime = 3600
action = iptables-docker-user
参数故意调保守(10 次/10 分钟才封 1 小时),合法用户输错几次密码不会被锁。上线几分钟后 Total failed 就开始增长——扫描器还在孜孜不倦。
4.4 密钥从 compose 明文移到 .env
检查时发现数据库密码、JWT secret 明文写在 docker-compose.yml 里(644 权限,谁都能读)。改为 ${VAR} 插值 + .env(chmod 600):
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGRST_DB_URI: ${PGRST_DB_URI}
PGRST_JWT_SECRET: ${PGRST_JWT_SECRET}
密码值不变 → 远程客户端零感知。别忘了把含旧密钥的 compose 备份文件也 chmod 600。
4.5 PostgREST 从 superuser 降为最小权限角色
这是收益最大的一项。原来 PostgREST 直接用 superuser postgres 连库——API 层的任何缺陷都等于数据库最高权限。改为标准的 authenticator 模式:
-- 低权登录角色:本身无任何表权限,只允许切换角色
CREATE ROLE authenticator LOGIN NOINHERIT PASSWORD '<强随机密码>';
GRANT web_anon TO authenticator; -- 匿名只读
GRANT web_user TO authenticator; -- JWT 认证后可写
PGRST_DB_URI 改用 authenticator,重建 postgrest 容器(数据库本身不用动,停机约 2 秒)。日志确认:
connection authenticated: identity="authenticator" method=scram-sha-256
4.6 收回 3080 的本机访问权(uid 白名单)
针对”本机跳板”这条路,用 iptables owner 模块按进程 uid 收口:
iptables -A OUTPUT -o lo -p tcp --dport 3080 -m owner --uid-owner ubuntu -j ACCEPT
iptables -A OUTPUT -o lo -p tcp --dport 3080 -m owner --uid-owner root -j ACCEPT
iptables -A OUTPUT -o lo -p tcp --dport 3080 -j DROP
验证:DSH 远程链路正常(走 nginx→3081,不经 3080),而 sudo -u nobody curl 127.0.0.1:3080 直接被 DROP。即使某个服务被攻破拿到低权 shell,也摸不到 DSH 的高权限接口。
4.7 取证后收回 rag 库公网映射
5432 要不要收回,用证据说话:
ss -tn state established '( dport = :5432 )' # 无远程客户端在线
grep POSTGRES_HOST /path/to/.env # 127.0.0.1——本地应用直连
证据表明这个库只有本机应用在用,公网上唯一的”访问者”是爆破扫描器。于是把 compose 端口改为 127.0.0.1:5432:5432 收回,本地应用验证无感。
最终验证矩阵
| 检查项 | 预期 | 实际 |
|---|---|---|
| DSH 根路径未认证 | 303 跳登录 | ✅ |
| DSH 登录页 | 200 | ✅ |
| 伪造 Host 打 RPC | 401 | ✅ |
| 业务库 18154 远程认证 | 正常登录 | ✅ |
| 公网访问 3000/5244/5432 | 不可达 | ✅ |
| 本机访问上述服务 | 正常 | ✅ |
| 静态博客 8081 | 200 | ✅ |
| fail2ban jail | 运行中,已捕获新爆破 | ✅ |
几点心得
- “不受漏洞影响”要靠验证,不是靠版本号。公告没列我的版本,但根因代码模式实测存在——真正决定风险的是部署形态和前置认证。
- docker 时代的防火墙直觉是错的。
ufw deny对 docker 发布端口无效,必须懂 DOCKER-USER;fail2ban 封禁同理。 - superuser 直连业务组件是最常见的隐性巨坑。PostgREST 用 postgres 超管连库这种配置,在无数项目里躺着。
- 加固可以全程零业务中断。所有动作(防火墙增量规则、.env 迁移、角色降权、端口收回)都是可验证、可回滚的小步操作,每步执行前后各跑一次验证矩阵。
- 黑名单式防火墙是心理安慰。154 条”封扫描器 IP”的规则挡不住明天的第 155 个扫描器,默认拒绝 + 白名单才是正解。
残余风险也诚实记录:业务库 superuser 仍可从 18154 远程登录(业务需要),下一步计划用 Tailscale 组网后把数据库端口彻底收回内网。安全没有终点,只有不断缩小的暴露面。