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 入站限速
验证
|
|
This PR has been inactive for 14 days and is marked stale. It will be closed in 14 days if there is no further activity. Push an update or comment to keep it open. |
背景
安全审计中危项(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