Skip to content

fix(security): 中危批次——本地代理 SSRF、terminal attach 限速、SVG XSS、frame 防护、分享脱敏、git 门控大小写、history 事件健壮性 - #514

Open
zayokami wants to merge 4 commits into
Stack-Cairn:mainfrom
zayokami:feat/security-p1-fixes

Conversation

@zayokami

@zayokami zayokami commented Aug 16, 2026

Copy link
Copy Markdown

背景

安全审计中危项(confirmed×2 为主)的 7 项修复(issue #515),每项带复现/回归测试(先在未修复代码上证明问题存在,修复后转绿)。

Closes #515

Screenshots / preview

本次改动为协议/运行时逻辑(SSRF 过滤、限速、响应头、脱敏、门控),无 UI 视觉变化;以下是各修复的复现测试证据(before/after):

P1 修复验证证据

修复内容

1. 桌面本地 /image-proxy SSRF(confirmed×2)

  • 漏洞:validate_image_proxy_url 只校验 scheme/host/凭据,127.0.0.1、内网、169.254.169.254 云元数据全部可访问;端点无 token、CORS *
  • 修复:URL 增加与网关 Go 侧对齐的字面 IP 黑名单(回环/私网/link-local/元数据/多播/保留/广播,含 localhost 主机名);并增加 Origin 来源校验(仅放行 Tauri WebView 来源,恶意网页 fetch 被拒)。<img> 路径不带 Origin,由 IP 黑名单兜底
  • 测试:SSRF 目标拒绝用例 + origin 白名单用例

2. /ws/v2/terminal 浏览器链路无界 attach(confirmed×2)

  • 漏洞:读循环无限速、attach 帧把任意长度/数量的 session_id 存进无界 map,单连接 OOM;对照主链路有 100fps 限速
  • 修复:入站限速与主链路同款(100fps/突发 200/3 次违规断连);attach 跟踪表容量(512)与 id 长度(512)上限
  • 测试:容量封顶 + 超长 id 拒绝用例

3. SVG XSS on gateway origin(confirmed×2)

  • 漏洞:未认证 /image-proxy 把攻击者 SVG 以 image/svg+xml 直出,无 CSP/Content-Disposition,顶层导航即在网关 origin 执行脚本窃取 localStorage 里的管理员 token
  • 修复:SVG 响应加 Content-Security-Policy: sandbox + Content-Disposition: inline,降级为纯图片源
  • 测试:SVG 响应头断言 + 非 SVG 不受影响

4. WebUI 点击劫持(finder)

  • 漏洞:无 X-Frame-Options/frame-ancestors,token 自动登录可被第三方 iframe 嵌入诱导点击
  • 修复:serveIndex 加 X-Frame-Options: DENY + frame-ancestors 'none'
  • 测试:index 响应头断言

5. Public-share 脱敏绕过(confirmed×2)

  • 漏洞:redact_tool_content 只处理 assistant/toolResult,role=summary 的归档段摘要(含工具输出浓缩)原样流出公开分享页
  • 修复:redactor 新增 summary 分支,redact 语义下整体替换为占位(摘要无法可靠剥离工具内容)
  • 测试:summary 泄漏用例(密钥/路径不得出现在输出)

6. enable_web_git 大小写绕过(1 plausible/1 refuted 的窄通道)

  • 漏洞:网关侧 gitActionIsWrite 精确匹配,桌面侧 to_ascii_lowercase,CLONE_START/Push 等变体过门控却在桌面执行写操作
  • 修复:网关侧 action 先 ToLower 再判写
  • 测试:TestV2GitRejectsCaseMutatedWriteActionsWhenDisabled(修复前:变体被放行=复现)

7. history.event 缺 conversation 字段崩溃(plausible×2)

  • 漏洞:网关可发不带 conversation 的 upsert,normalizeGatewayConversationSummary(undefined) 抛 TypeError 中断后续监听者投递
  • 修复:跳过无 conversation 的 upsert 帧;reconciliation 对 message_count 加 optional chain
  • 测试:web-sidebar-backend 用例(修复前:抛异常=复现)

回归

结果
Go(网关) ✅ 全过(仅基线 Windows 权限测试,与本次无关)
Rust(桌面) ✅ 改动域 129 个测试全过
WebUI ✅ 579 个测试全过
lint / diff --check ✅ 干净(仅仓库既有 CRLF)

行为变更说明

  • 桌面本地代理不再代理内网/回环图片 URL(模型输出的公网图片不受影响)
  • 分享开启"脱敏工具内容"时,历史段摘要将被整体脱敏替换
  • WebUI 页面禁止被 iframe 嵌入

🤖 Generated with Claude Code

…脱敏、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>
@StackCairn
StackCairn marked this pull request as draft August 16, 2026 11:23
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

PR governance checks passed. Awaiting human review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@su-fen

su-fen commented Aug 16, 2026

Copy link
Copy Markdown
Member

审查结论:暂不建议直接合入,当前有两个安全阻塞问题。

1. 桌面 /image-proxy 的 SSRF 修复仍可绕过

crates/agent-gui/src-tauri/src/services/proxy.rsvalidate_image_proxy_url 只拦截字面 IPv4 黑名单和 IPv6 回环 ::1,请求随后交给默认会跟随重定向的共享 reqwest client。因此以下路径仍可访问本机或内网:

  • 攻击者域名解析到 127.0.0.1169.254.169.254 或 RFC1918 地址;
  • 公网 URL 通过 30x 跳转到内网地址;
  • fc00::/7fe80::/10 等内网/link-local IPv6;
  • IPv4-mapped IPv6,例如 http://[::ffff:127.0.0.1]/

这意味着原 SSRF 边界没有完整封住。建议在实际连接阶段检查 DNS 解析出的全部地址,并在每次重定向时重新校验;同时覆盖完整 IPv6 禁止段并先 unmap IPv4-mapped IPv6。需要增加 DNS、重定向和 IPv6 绕过回归测试。

2. terminal ID 长度限制仍可造成单连接高内存占用

crates/agent-gateway/internal/protocol/pbws/terminal_conn.goremember 先执行 strings.TrimSpace,再检查 512 字节长度并把结果放入 map。Go 的字符串切片会继续持有原字符串的整块底层内存,因此攻击者可以发送“接近 1 MiB 的空白前缀 + 短且唯一的 ID”;trim 后长度合法,但每个 map key 仍可能保留接近 1 MiB。填满 512 个 key 后,单连接仍可长期保留数百 MiB,原 OOM/内存 DoS 风险并未真正消除。

建议在 trim 前限制原始字段长度,对规范化后的 key 使用有界复制,并在超长或容量已满时明确拒绝该 attach、不要继续转发给 Agent。现有测试只覆盖无空白的超长字符串,未覆盖这一保留内存路径。

其他合入检查

  • 与最新 main 的临时合并无冲突;
  • WebUI 构建成功,测试 580/580 通过;
  • Rust 相关定向测试 21/21 通过,但没有覆盖上述绕过;
  • cargo fmt --all --check 在本 PR 新增 Rust 代码上有 3 处格式差异;
  • 本机没有 Go 工具链,因此未独立复跑 Go 测试。

建议修复以上两个安全问题、补齐绕过测试并执行格式化后再合入。

…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>
@zayokami

Copy link
Copy Markdown
Author

已按审核意见修订两个安全阻塞问题,新 SHA 已推送(f1d7510)。

1. /image-proxy SSRF 完整封堵

  • IPv4-mapped IPv6:is_blocked_image_proxy_ip 统一入口先 to_ipv4_mapped() 还原再走 IPv4 黑名单(对齐网关 outbound_http.goUnmap() 先例);http://[::ffff:127.0.0.1]/[::ffff:169.254.169.254]/ 均被拒绝,测试覆盖。
  • 完整 IPv6 禁止段:与 Go 侧 blocked prefixes 逐项对齐——::/::164:ff9b::/96(NAT64)、64:ff9b:1::/48100::/642001::/232001::/32(Teredo)、2001:2::/482001:10::/282001:20::/282001:db8::/322002::/16(6to4)、3fff::/205f00::/16fc00::/7(ULA)、fe80::/10ff00::/8
  • DNS rebinding:连接前对主机名做 tokio::net::lookup_host 全地址解析,任一命中黑名单即拒绝,fail-closed(解析失败也拒绝);测试覆盖 localhost.(尾点,不命中字面拦截、解析命中回环)与无效域名。
  • 重定向:reqwest 0.13 的重定向策略是 client 级配置,image-proxy 改走 system_proxy::async_client_builder(同代理语义)并设置 custom redirect policy——每次 30x 跳转重新校验目标(字面 IP/localhost/凭据/scheme/主机名解析)。测试覆盖每跳校验的各拒绝路径与公网放行。

2. terminal attach ID 内存保留

  • remember 先按原始字段长度(4 KiB)拒绝再 trim——"接近 1 MiB 空白前缀 + 短 ID"在 trim 前即被拒,不再进入 map;规范化后 ≤512 并做有界拷贝(string([]byte(...)))切断与整帧缓冲区的引用。
  • 超长/空 id/容量已满均返回明确错误,handleFrame 回错误帧且不再转发给 Agent(此前超长被静默丢弃、超限仍转发)。
  • 测试:空白前缀绕过用例(旧实现会接受)、handleFrame 拒绝路径验证(队列只有拒绝帧而非转发路径的 offline 帧)、容量满拒绝、合法 attach 不受影响。

格式化:cargo fmt --check 现为 0 差异(修复了原 3 处)。

回归:Rust 受影响域 46/46(含 proxy 22 个测试);Go internal/... + test/websocket 全过,全量仅既有 Windows 权限位基线失败;WebUI 未触及。

@su-fen

su-fen commented Aug 17, 2026

Copy link
Copy Markdown
Member

复审结论:暂不建议直接合入。当前 PR 可合并且 CI 已全绿,但仍有两个安全阻塞问题。

1. 桌面 /image-proxy 的 DNS 校验没有绑定到实际连接,SSRF 仍可被 DNS rebinding 绕过

crates/agent-gui/src-tauri/src/services/proxy.rs 中,初始请求先通过 image_proxy_host_resolves_to_blocked 独立解析并检查地址,之后由新建的 reqwest client 执行 .send();client 没有配置使用前面得到的已校验地址,因此实际连接会再次解析域名。攻击域名可以在预检查时返回公网 IP、在实际连接时返回 127.0.0.1、RFC1918 或云元数据地址。

重定向路径也有同样问题:redirect policy 内的 to_socket_addrs() 只是一轮预检查,follow 后实际连接仍会再次解析。启用应用代理时,目标域名还可能由 HTTP/SOCKS 代理端解析,客户端预检查与实际目标进一步脱钩。

建议把 IP 黑名单放进实际连接使用的 DNS resolver/connector,并让 connector 只使用本轮已审核的地址;重定向每一跳都必须走同一安全 resolver。若需要继续支持应用代理,还需要明确代理模式下如何保证目标解析和连接同样受限。回归测试应使用可控 resolver 验证“第一次公网、第二次内网”的切换,而不只是单测预检查函数。

2. terminal 入站限速可被文本帧绕过,且发生在反序列化之后

crates/agent-gateway/internal/protocol/pbws/terminal_conn.goreadTerminalFrame 会对所有非二进制 WebSocket 消息直接 continue,而 rateLimiter.Allow() 只在该函数返回一个已完成 protobuf 反序列化的二进制帧后执行。因此:

  • 握手前可无限发送文本帧,占用 terminal connection slot,完全不经过限速;
  • 认证后文本帧仍不计入限速;
  • 大型二进制帧先完成读取和 protobuf 反序列化,之后才被限速丢弃,无法真正限制解析 CPU。

建议在每次 ReadMessage() 成功后立即计数,非二进制帧直接拒绝或同样计入限速;握手阶段也应有超时/帧率边界。请补充文本帧绕过以及“限速发生在反序列化前”的回归测试。

验证情况

  • GitHub 8 项状态检查全部通过,merge state 为 CLEAN
  • 本地 Rust 相关测试 23/23 通过,cargo fmt --all --checkgit diff --check 通过;
  • WebUI 新增回归用例 8/8 通过,生产构建通过;
  • 本机无 Go 工具链,Gateway Go 测试以 CI 全绿为准。

请修复以上两个安全边界问题后再合入。

…化前并覆盖文本帧

按 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>
@zayokami

Copy link
Copy Markdown
Author

两个安全阻塞问题已修复(52f818f5),CI 待跑。

1. image-proxy DNS rebinding

已将 IP 黑名单装进 reqwest 实际拨号使用的 DNS resolver(ImageProxyResolver,proxy.rs):

  • 绑定原理:hyper 对主机名目标每次新建连接都调用该 resolver,且只拨号本轮回填的地址——审核结果与连接目标一一对应,不存在第二次独立解析。重定向跨 origin 的每一跳必然新建连接,自动重走同一安全 resolver 再审;同 origin 跳转复用的是当初已过审的连接本身,无重新解析窗口。
  • fail-closed:任一地址命中黑名单即整体拒绝(不允许从混合答案中挑出内网地址),空答案/解析失败同样拒绝。字面 IP 不经 resolver(hyper 短路),仍由 validate_image_proxy_url 与重定向策略的字面量检查拦截。
  • 代理模式:socks5h 已翻转为 socks5(system_proxy::async_client_builder_local_dns)——目标经安全 resolver 本地解析后把 IP 交给代理,解析与连接保持绑定;图片代理本就本地预解析目标,无新增 DNS 暴露。HTTP 代理(CONNECT/absolute-URI)协议只携带主机名,目标解析确在代理端完成、客户端无法绑定,残余窗口由前置预检 + 每跳重定向预检收缩,触达面限于代理所在网络位置;已在 ImageProxyResolver 注释中明确说明。已启用代理的主机名(用户设置,非攻击者输入)在 resolver 中豁免,避免回环/内网代理端点被误杀导致图片代理整体不可用。
  • 回归测试:脚本化可控 resolver 验证"第一次公网、第二次内网"翻转在拨号前被拒;混合/空/失败答案 fail-closed;代理主机豁免不扩散。集成测试以本地回环 HTTP 服务器证明被否决的解析不产生任何实际连接(对照组放行谓词下同一地址可达)。

2. terminal 入站限速

  • 计数点移到每次 ReadMessage() 成功后、proto 反序列化前(readTerminalFrame 内):文本帧同罪计入;超限帧在解析前即丢弃(denied 三态返回,浏览器角色回 "too many requests");连续违规超阈值判死。
  • 握手阶段:限速器自第一帧起生效,浏览器角色握手后沿用同一预算(攻击者不能借完成握手重置额度);新增 10s 握手绝对读超时(terminalHandshakeTimeout),空连接/慢速流同样被边界。
  • 主链路 browser_conn.readFrame 存在完全相同的形态(文本帧 continue 不计数、Allow 在 Unmarshal 之后),已同款修复——否则两条链路语义不一致。
  • Agent 数据面(已认证桌面端)未加帧率限制:输出帧没有可容忍的丢帧语义(静默丢弃会让终端显示残缺),其入站由 read limit + 会话通道背压兜底,注释中已说明。如需对 Agent 角色也设上限,建议后续单独设计(如输出合并),不在本轮扩大改动面。
  • 回归测试:文本帧耗尽突发额度后合法帧被拒且三次违规判死;非法 protobuf 帧在反序列化前被丢弃(连接存活、回填后后续帧正常解析——旧顺序下连接已关闭);pre-hello 300 帧文本洪泛被判死;沉默连接被握手超时关闭;合法浏览器 hello 正路放行;主链路读路径同款两个测试。

验证

  • Rust proxy/system_proxy 相关 41/41 通过,cargo fmt --all --check 干净;
  • Gateway:go build/go vet 干净,pbws 包全部测试通过,golangci-lint run ./... 0 issues(本机已有 Go 工具链,补齐了上一轮的本地验证缺口);
  • git diff --check 干净。
  • 本机 Windows 上 agenttoken 文件权限测试与 12 项 git worktree/hook Rust 测试在干净树上同样失败,为既有环境限制,与本次改动无关。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

安全审计中危批次:本地代理 SSRF、terminal attach 无界、SVG XSS、分享脱敏绕过、git 门控大小写、history 事件健壮性

2 participants