Skip to content

2.6.1

Choose a tag to compare

@westanke westanke released this 23 Oct 15:16
· 36 commits to main since this release

2.6.1 · SEC-H11 SSRF 与凭据外泄修复 + 设备身份自动识别(2026-10-23)

基于:v2.6.0(含 settings-namespaces-v1 能力位补漏)。这是一次安全修复,
起因是用户问「看桥还有其他的漏洞吗」——于是把审计做成实测而非推理:直接 curl 打
运行中的桥,用真实凭据验证攻击链是否成立。

发现过程

问题不在命名空间写策略,而在旁边那条专用路由。SECURITY.md 明写 llm-*
(供应商、模型、Base URL)只读,理由是「手机能改 Base URL = 把模型请求引到攻击者的
端点」;但 isSettingsNamespaceWriteAllowed 只被 mutateNamespace 调用,
createProvider/updateProvider/discover 三个写入口一次都不检查。同一个目标,
一条路堵死,另一条路敞着。

比「能改配置」更糟的是 discover:它把 baseURL 交给内核发 GET {baseURL}/models,
内核带着该供应商已配置的真实 key。实测起一个 python http.server,对官方
deepseek(DEEPSEEK_API_KEY 已配置)发起 discover,服务端日志里直接收到:

GET /v1/models
authorization: Bearer sk-…(真实生产 key)
user-agent: deepseek-harness/0.2.1-rc.2

也就是说这不是 SSRF 而已,是凭据外泄:攻击者只要让受害者点一下「发现模型」,
或者自己持 settings.write 直接调,就能拿到明文 key。

三个入口都没拦:createProvider 只判 input.baseURL.trim() === '',
updateProvider 只判 value === null || value === '',discover 原样透传。

持久化变体更彻底:PATCH /settings/providers/{id} 改 baseURL 会真写进配置,
此后每次聊天都把 key 打到新地址。而 router.ts 的审计只记 providerId 不记地址
(discover 的 baseURL 更是临时覆盖、不落配置),所以事后无法从审计追溯凭据被
打到哪个地址
—— 取证盲区。

零测试覆盖:grep baseURL tests/*.spec.ts 找不到任何 ssrf/private/169.254 断言,
tests/settings-adapter.spec.ts:79-86 只验了 https://api.example/v1 这条正常路径。

顺带发现配对默认权限自相矛盾:WebUI「生成配对码」默认勾满七档 scope(含
settings.write),而「生成配置文本」那条路径刻意只给五档(注释
No scopes on purpose)。CHANGELOG-DEV.md:2913-2915 又明写 settings.write
「是用户显式要求才给的东西」——三处声明与默认值互相打架。

修了什么

① 地址策略(新文件 src/host/provider-url-policy.ts,186 行)

三道闸门,三个写入口全部生效:

# 判据 拒绝码 挡的是什么
1 绝对 URL + 协议限 http/https 400 PROVIDER_BASE_URL_INVALID file:/gopher:/data: 这类由 URL 解析器自己发起的协议
2 私网/回环/链路本地/CGNAT 默认拒绝 403 PROVIDER_BASE_URL_FORBIDDEN SSRF:内网探测、云元数据 169.254.169.254
3 已存凭据的供应商换 host 必须同时给新 key 403 PROVIDER_BASE_URL_REPOINT_FORBIDDEN 换成攻击者的公网域名 —— 闸门 2 对此无能为力

闸门 3 是实施中补的、审计报告里没有的:只拦私网不够。攻击者不需要内网,
他只需要一个公网域名 —— 内核照样会把 key 发过去。真正的不变量是「不能让内核把
已有凭据发到新目的地」。所以只在 baseURL 的 host 真的变了、且该供应商
credentials.describe 报 configured: true 时才查,纯改 models/显示名/同 host 改
path 都是零额外开销的日常操作。

allowPrivateProviderHosts 放行自建 LLM(ollama 默认 127.0.0.1:11434)。两种粒度:
带端口条目按 host:port 精确比对(推荐),裸 host 条目放行该主机全部端口。不支持
CIDR —— 只接受精确 host。理由:放行 192.168.1.5 等于放行它上面所有服务,而 ollama
只在 11434。

刻意不做 DNS 解析:校验时解析一次、内核发请求时再解析一次,中间可变(TOCTOU),
而且会给出「已经校验过」的错觉。攻击面因此被收窄到「攻击者必须控制一个公网域名」,
而不是「攻击者必须控制 DNS」。