fix(security): 中危批次——本地代理 SSRF、terminal attach 限速、SVG XSS、frame 防护、分享脱敏、git 门控大小写、history 事件健壮性 - #514
fix(security): 中危批次——本地代理 SSRF、terminal attach 限速、SVG XSS、frame 防护、分享脱敏、git 门控大小写、history 事件健壮性#514zayokami wants to merge 4 commits into
Conversation
…脱敏、git 门控大小写、history 事件健壮性 安全审计中危(confirmed×2 为主)的 7 项修复,各带复现/回归测试: 1. 桌面本地 /image-proxy SSRF(confirmed×2):validate_image_proxy_url 增加 与网关 Go 侧对齐的 IPv4/IPv6 字面 IP 黑名单(回环/私网/link-local/云元数据/ 多播/保留/广播,含 localhost 主机名),并增加 Origin 来源校验(只允许 Tauri WebView 来源,恶意网页 fetch 被拒)。<img> 路径由 IP 黑名单兜底。 - 测试:rejects_ssrf_targets_in_image_proxy_urls + origin 用例 2. /ws/v2/terminal 浏览器链路无界 attach(confirmed×2):读循环加入与主链路 同款 InboundRateLimiter(100fps/突发 200/3 次违规断连);attach 跟踪表 增加容量(512)与 id 长度(512)上限,杜绝单连接以 1MiB/帧无限增长内存。 - 测试:TestTerminalBrowserRememberCapsTrackedIDs / RejectsOversizedIDs 3. SVG XSS on gateway origin(confirmed×2):image-proxy 对 SVG 响应加 Content-Security-Policy: sandbox + Content-Disposition: inline,顶层 导航到 SVG 不再能在网关 origin 执行脚本(WebUI token 在 localStorage)。 - 测试:SVG sandbox CSP 用例 + 非 SVG 无 CSP 用例 4. WebUI 点击劫持(finder):serveIndex 加 X-Frame-Options: DENY + CSP frame-ancestors 'none'(WebUI token 自动登录,禁 iframe 内嵌)。 - 测试:root index 响应头断言 5. Public-share 脱敏绕过(confirmed×2):redact_builtin_tool_content_json 新增 role=summary 分支——归档段摘要是 LLM 对含工具输出窗口的浓缩, 无法可靠剥离工具内容,redact 语义下整体替换为占位。 - 测试:summary 泄露用例(secret/路径不得出现在输出中) 6. enable_web_git 大小写绕过(1 plausible/1 refuted 的窄通道):网关侧 git action 先 ToLower 再判写操作,CLONE_START/Push 等变体不再绕过门控。 - 测试:TestV2GitRejectsCaseMutatedWriteActionsWhenDisabled (修复前:大小写变体被放行转发=复现) 7. history.event 缺 conversation 字段崩溃(plausible×2):webSidebarBackend 跳过无 conversation 的 upsert 帧(不再 TypeError 中断后续监听者投递); useGatewayHistoryReconciliation 对 message_count 加 optional chain。 - 测试:web-sidebar-backend 用例(修复前:抛异常=复现) 回归:Go 全量(仅基线 Windows 权限测试)、Rust 改动域 129 个、WebUI 579 个 全过;lint 仅仓库既有 CRLF 格式误报。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
PR governance checks passed. Awaiting human review. |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
审查结论:暂不建议直接合入,当前有两个安全阻塞问题。 1. 桌面
|
…ttach 内存保留 针对 PR Stack-Cairn#514 审核的两个安全阻塞问题: 1. 桌面 /image-proxy SSRF 完整封堵(原修复仅拦截字面 IPv4 与 ::1): - is_blocked_image_proxy_ip 统一入口:IPv4-mapped IPv6 先 to_ipv4_mapped() 还原再走 IPv4 黑名单;IPv6 段与网关 Go 侧 outbound_http.go 逐项对齐 (::/::1、64:ff9b::/96、64:ff9b:1::/48、100::/64、2001::/23、2001::/32、 2001:2::/48、2001:10::/28、2001:20::/28、2001:db8::/32、2002::/16、 3fff::/20、5f00::/16、fc00::/7、fe80::/10、ff00::/8) - 连接前 DNS 解析(image_proxy_host_resolves_to_blocked):主机名解析出的 全部地址任一命中黑名单即拒绝,fail-closed 封 DNS rebinding - 每次 30x 跳转重新校验(validate_image_proxy_redirect_target):字面 IP、 localhost、凭据、scheme 与主机名解析全查;reqwest 0.13 重定向策略是 client 级配置,image-proxy 改走 system_proxy::async_client_builder 自定义 redirect policy - 测试:IPv6/mapped 拒绝与公网放行、redirect 每跳校验(含 "localhost." 尾点解析)、DNS 预检(localhost 解析拒绝、无效域名 fail-closed) 2. terminal attach ID 内存保留(Go 子切片持有整帧缓冲): - remember 先按原始字段长度(4 KiB)拒绝再 trim,杜绝"接近 1 MiB 空白 前缀 + 短 ID"把整帧内存挂在 map key 上;规范化后 ≤512 并做有界拷贝 切断与帧缓冲的引用 - 容量已满 / 超长 / 空 id 均返回明确错误,handleFrame 回错误帧且不再 转发给 Agent(此前静默丢弃) - 测试:空白前缀绕过用例(修复前会接受)、handleFrame 拒绝路径不回转发 (队列只有拒绝帧而非 offline 帧)、容量满拒绝、合法 attach 不受影响 另:cargo fmt 修复本 PR Rust 代码的 3 处格式差异(--check 现为 0)。 回归:Rust 受影响域 46/46;Go 全量仅既有 Windows 权限基线失败;WebUI 未触及。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
已按审核意见修订两个安全阻塞问题,新 SHA 已推送(f1d7510)。 1. /image-proxy SSRF 完整封堵
2. terminal attach ID 内存保留
格式化: 回归:Rust 受影响域 46/46(含 proxy 22 个测试);Go |
|
复审结论:暂不建议直接合入。当前 PR 可合并且 CI 已全绿,但仍有两个安全阻塞问题。 1. 桌面
|
…化前并覆盖文本帧 按 Stack-Cairn#514 复审意见修复两个安全阻塞问题: 1. image-proxy SSRF(DNS rebinding):预检与实际连接脱钩——预检独立解析后, reqwest client 拨号时会再次解析域名,攻击者可先答公网、连接时改答 127.0.0.1/RFC1918/云元数据。现将 IP 黑名单装进 reqwest 实际拨号使用的 DNS resolver(ImageProxyResolver):hyper 每次新建连接都经它解析且只拨号 本轮回填的地址,审核与连接一一绑定;重定向跨 origin 每跳的新连接自动重审; 混合答案/空答案/解析失败全部 fail-closed。字面 IP 不经 resolver,仍由 validate_image_proxy_url 与重定向策略的字面量检查拦截。 代理模式:socks5h 翻转为 socks5(system_proxy 新增 async_client_builder_local_dns),目标经安全 resolver 本地解析后把 IP 交给代理,解析与连接保持绑定(图片代理本就本地预解析目标,无新增 DNS 暴露);HTTP 代理(CONNECT/absolute-URI)协议只携带主机名,目标解析在 代理端完成,客户端无法绑定,残余窗口由前置预检 + 每跳重定向预检收缩, 触达面限于代理所在网络位置(已在代码注释与本文档明确)。已启用代理的 主机名来自用户设置,resolver 对其豁免,避免回环/内网代理端点被误杀。 回归:脚本化 resolver 验证"第一次公网、第二次内网"翻转在拨号前被拒、 混合/空/失败答案 fail-closed、代理主机豁免不扩散;集成测试用本地回环 HTTP 服务器证明被否决的解析不产生任何实际连接(对照组放行谓词可达)。 2. terminal 入站限速绕过:readTerminalFrame 对非二进制帧直接 continue 且 rateLimiter.Allow 在 proto 反序列化之后执行——握手前文本帧洪泛零成本占 slot、认证后文本帧不计数、大帧先解析后限速无法约束解析 CPU。现将计数点 移到每次 ReadMessage 成功后、反序列化前:文本帧同罪计入,超限帧在解析前 丢弃,连续违规判死;握手阶段新增限速器(自第一帧生效,浏览器角色握手后 沿用同一预算)与 10s 绝对读超时(terminalHandshakeTimeout)。主链路 browser_conn.readFrame 存在完全相同的形态,同款修复(握手已有 idle 读 超时)。Agent 数据面不限速(nil limiter):输出帧无丢帧语义,其入站由 read limit 与会话通道背压兜底,对端为凭证认证的桌面端。 回归:文本帧耗尽突发额度后合法帧被拒并三次违规判死、非法 protobuf 大帧 在反序列化前被丢弃(连接存活、回填后后续帧正常)、pre-hello 300 帧文本 洪泛被判死、沉默连接被握手超时关闭、合法浏览器 hello 正路放行;主链路 读路径同款两个测试。 验证:Rust proxy/system_proxy 41/41 通过,cargo fmt --all --check 干净; Gateway go build/vet 干净,pbws 包全部测试通过,golangci-lint 0 issues; git diff --check 干净。(本机 Windows 上 agenttoken 文件权限测试与 git worktree/hook 共 12 项 Rust 测试在干净树上同样失败,为既有环境限制, 与本次改动无关。) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
两个安全阻塞问题已修复(52f818f5),CI 待跑。 1. image-proxy DNS rebinding已将 IP 黑名单装进 reqwest 实际拨号使用的 DNS resolver(
2. terminal 入站限速
验证
|
背景
安全审计中危项(confirmed×2 为主)的 7 项修复(issue #515),每项带复现/回归测试(先在未修复代码上证明问题存在,修复后转绿)。
Closes #515
Screenshots / preview
本次改动为协议/运行时逻辑(SSRF 过滤、限速、响应头、脱敏、门控),无 UI 视觉变化;以下是各修复的复现测试证据(before/after):
修复内容
1. 桌面本地 /image-proxy SSRF(confirmed×2)
validate_image_proxy_url只校验 scheme/host/凭据,127.0.0.1、内网、169.254.169.254云元数据全部可访问;端点无 token、CORS *localhost主机名);并增加 Origin 来源校验(仅放行 Tauri WebView 来源,恶意网页 fetch 被拒)。<img>路径不带 Origin,由 IP 黑名单兜底2. /ws/v2/terminal 浏览器链路无界 attach(confirmed×2)
3. SVG XSS on gateway origin(confirmed×2)
/image-proxy把攻击者 SVG 以image/svg+xml直出,无 CSP/Content-Disposition,顶层导航即在网关 origin 执行脚本窃取 localStorage 里的管理员 tokenContent-Security-Policy: sandbox+Content-Disposition: inline,降级为纯图片源4. WebUI 点击劫持(finder)
X-Frame-Options: DENY+frame-ancestors 'none'5. Public-share 脱敏绕过(confirmed×2)
redact_tool_content只处理 assistant/toolResult,role=summary的归档段摘要(含工具输出浓缩)原样流出公开分享页6. enable_web_git 大小写绕过(1 plausible/1 refuted 的窄通道)
gitActionIsWrite精确匹配,桌面侧to_ascii_lowercase,CLONE_START/Push等变体过门控却在桌面执行写操作ToLower再判写TestV2GitRejectsCaseMutatedWriteActionsWhenDisabled(修复前:变体被放行=复现)7. history.event 缺 conversation 字段崩溃(plausible×2)
normalizeGatewayConversationSummary(undefined)抛 TypeError 中断后续监听者投递回归
行为变更说明
🤖 Generated with Claude Code