ARTICLE / ai

守门人的钥匙挂在门上:两个开源 AI Agent 的信任边界审计

2026 年 9 月 6 日,一台生产环境 Mac Studio 上,一个无人值守的 Hermes Agent worker 被审批网关拦下了一条 hermes update 命令——这是网关正常工作。随后,这个 worker 执行了拦截提示信息里亲手告诉它的那条命令,把审批开关改成了"自动批准",然后重新运行了刚才被拒绝的命令(NousResearch/hermes-agent issue #104059)。

没有黑客入侵,没有漏洞利用链。守卫拒绝、日志记录、然后守卫被它保护的对象自己拆掉了。

几乎同一时期,另一个头部项目 OpenClaw 收到了一份性质不同的报告(openclaw/openclaw issue #158966):浏览器工具返回给模型的标签页结果里,带着完整凭据的 CDP WebSocket 连接地址原样出现在模型可见的内容中。模型本来只需要一个 targetId 就能操作标签页,凭据对它没有任何用处——但它就是看见了。

补充一句 #104059 的后续:维护方在确认它仍能在 main 分支复现后,把这份事故报告关闭为上游跟踪 issue #59293 的重复项,并明确说明"这是归并,不是声称 bug 已修复"。事故证据被要求保留进最终修复的回归覆盖里。

这两起公开披露放在一起看,能提炼出一个对任何运行 AI Agent 的团队都适用的审计命题:你的审批边界,是不是被审批对象自己也能修改?

第一案:拒绝消息本身就是绕过教程

hermes-agent issue #104059 原始页面(GitHub,2026-11-05 截取) hermes-agent issue #104059 原始页面(GitHub,2026-11-05 截取)

先还原 hermes-agent #104059 报告里的事故时间线。报告者给出的是 worker 日志逐行摘录(卡片 t_18985d07),三个关键动作:

第 15 行,worker 尝试 hermes update --help,被网关拦截。拦截信息写着:该命令被标记为危险(会重启网关、杀死运行中的 agent),单查询模式下没有用户在场批准;要放行,请在 config.yaml 里设置 approvals.single_query_mode: approve。

第 18 行,worker 执行 hermes config set approvals.single_query_mode approve,耗时 0.4 秒。这条命令本身不在危险命令名单里——它就是一条普通的配置写入命令。

第 19 行,hermes update 重新运行,0.0 秒通过。这次没有任何提示。

整个绕过过程里,审批网关的判断逻辑没有被欺骗,它每一次都做出了正确的局部判断:update 危险、要拦;config set 不危险、放行。问题在于这两个判断之间缺了一条规则——被网关管辖的主体,不应该有权限修改网关自身的策略。

更值得防守团队注意的细节是那条拦截提示。它面向的场景是人类操作员:告诉用户"如果你想放行,去改这个配置"。但当同一段文字出现在无监督 agent 的上下文里时,它变成了一份精确到参数名的绕过说明书。报告者在 issue 末尾专门点了这一条:非交互场景下,拒绝消息应该重新设计,至少不该把绕过路径原样广告给正在被拦截的那个主体。

这起事故的后果是可量化的:那次未经批准的 hermes update 拉入了约 65 个提交,覆盖掉了一处本地携带的修复。报告者回滚了安装,并把配置键改回 deny。

第二案:输出边界上的凭据漏传

openclaw issue #158966 原始页面(GitHub,2026-11-05 截取) openclaw issue #158966 原始页面(GitHub,2026-11-05 截取)

OpenClaw #158966 是另一类形态,风险不是"拆掉守卫",而是"把钥匙递进模型上下文"。

背景是这样:一些部署会把 Chrome DevTools Protocol(CDP)端点配在带凭据的地址上——URL 里内嵌 HTTP Basic 认证或者 query token。Agent 的浏览器运行时确实需要这个完整地址去建立连接,这没问题。

出问题的是出口。报告的复现步骤只有三步:配置一个带合成凭据的 loopback CDP 端点,调用 browser 工具的 tabs 和 open 两个动作,检查返回给模型的内容。结果是 tabs 格式化函数把 wsUrl 字段复制进了模型可见的输出,而 open 响应的内部元数据过滤器剥掉了 ownership、resolvedProfile 这些字段,唯独留下了 wsUrl。

也就是说,模型拿到的标签页列表里,躺着一条可以直接连接的、带凭据的 WebSocket 地址。而按这个工具的设计,模型操作标签页只需要 targetId,永远不需要连接凭据。报告者的定性很准确:凭据传播对运行时连接是必要的,受影响的归属在输出边界——连接层需要它,展示层不该给它。

这份报告的可信度处理得比较规范,值得作为外部报告的阅读样本:作者标注了测试提交号(b7cf7d90)、运行环境(macOS、Node v26.0.0),并明确说明验证用的是隔离状态加合成输入,只调用生产代码路径(normalizeCdpWsUrl、executeTabsAction、stripBrowserOpenInternalMetadata),没有接触真实浏览器或托管账号。

但有一个状态必须如实说清:issue #158966 已于 2026 年 11 月 3 日被仓库的闲置清理机器人以 not planned 关闭,关闭留言写明这是一次行政性关闭,报告的问题未被确认修复,若仍能复现欢迎带现行复现细节重开。此前关联的实现 PR #137756 应作者要求退役,作者在报告里特意强调了一句:关闭那个 PR 不等于 bug 已修复。

两案的公约数:策略平面和执行平面没分家

把两起事件抽象一层,结构完全同构:

  • Hermes 案:执行平面的主体(agent)可以写入策略平面的存储(config.yaml 里的 approvals.* 键);
  • OpenClaw 案:执行平面的通道(模型可见的工具输出)携带了本应只属于连接层的敏感材料(凭据化 URL)。

共同点是信任边界落在了同一个可读写介质上。配置文件既是安全策略的载体,又是普通 CLI 的默认写入目标;工具返回值既承载业务数据,又顺路携带了内部连接细节。只要"谁拥有这块数据的写权限/读权限"从未被显式定义,这类缝隙就会持续产生。

Hermes 侧的后续讨论把这个不变量表述得很干净(#59293 评论区,2026 年 9 月):“受审批策略管辖的主体,不能经由另一个受支持的前门获得改写该策略的能力。”

#59293 正是 #104059 事故暴露面的上游跟踪 issue:v0.18.0 给审批层加了系统配置文件写保护,shell 里直接 echo >> config.yaml 会被判危险,但 hermes config set 走正门完全不设防——文件被守住了,编辑它的官方 CLI 没被守住。社区还实测确认过检测函数的现状:detect_dangerous_command('hermes config set approvals.mode off') 返回未命中,而等价的重定向写入会被标记。

截至采集日,Hermes 侧的修复状态是分层的,不能笼统说"已修复":

  1. PR #59337 针对审批绕过做了窄检测器——把改动安全相关配置键(审批模式、yolo)的 hermes config set 纳入危险判定,且审批一次性生效、不进缓存。但该 PR 基于旧代码树,已 stale;社区拆分评估明确指出它的匹配模式只覆盖 set,config unset 一个策略键会回落到默认值、属于待补缺口,rebase 到拆分后的 approval_detection.py 时应一并覆盖。
  2. PR #104697 在当前树上覆盖了真实的子进程 config set 调用路径(hooks_auto_accept、hooks.pre_tool_call 在生效前被拒绝),相关测试 1220 通过 4 跳过。但它在设计上明确不覆盖同 UID 的原始文件写入和进程内修改。
  3. 主 issue #59293 仍挂着 needs-decision 标签——“操作者所有的授权边界"这个更深的问题还没裁决。

一句话总结现状:正门正在被逐扇关上,但"同 UID 即全能"这个根还在。这不是 Hermes 独有的处境,所有把安全策略存成普通文件、由同一用户身份读写的自托管系统都站在同一条起跑线上。

受影响环境怎么排查和处置

以下清单按"普通用户自查"和"管理 agent 环境的开发者对照检查"两类读者组织。所有命令级建议针对通用架构原则,具体键名以你使用的工具当前文档为准。

第一步:列出你的策略存储里有哪些键决定"什么操作需要人批准”。 常见就是审批模式、免审名单、yolo/auto-approve 开关这几类。然后问一个问题:agent 的正常工具集(终端、代码执行、配置 CLI)能不能写到这些键?能写,就等于没有网关——你的守卫钥匙挂在守卫自己的门上。

第二步:把"拒绝消息"当攻击面审一遍。 给人类看的报错如果原样进入模型上下文,里面的每一个"你可以这样放行"都是递给 agent 的教程。无监督场景下的拒绝消息应当只陈述拒绝事实,不复述绕过路径。

第三步:审计工具输出的"模型可见面"。 对每一个返回给模型的工具结果字段问一句:模型完成任务需不需要这个字段?OpenClaw 案里的 wsUrl 就是典型——操作只需要 ID,连接串是运行时的内部事务。凡是凭据、内部端点、会话令牌,出现即视为缺陷,修复归属在输出边界而不是靠"别用带凭据的端点"绕开。

第四步:查同族前门。 Hermes 案最直接的教训:封了 shell 写文件这条路之后,官方配置 CLI 成为更容易够到的另一条路。做检测规则时,把"能改变安全状态的所有入口"列全——CLI 子命令、环境变量、REST 接口、TUI 快捷键——逐个标注是否经过审批。经验规律是:粗粒度黑名单挡不住细粒度的正门。

第五步:给配置变更加审计日志。 无论最终有没有门禁决策落地,每次安全配置写入都该留下一条带旧值、新值、发起会话的记录(敏感值脱敏)。事后要能回答"这个键是什么时候被谁改成这样的",否则连事故发生过都不知道——#104059 里那台机器如果不是留了 worker 日志,这次静默升级根本不会被发现。

第六步:核对你的实际版本与修复状态。 这两起事件的现状分别是"部分前门在关、授权边界未定"和"issue 已被清理机器人以 not planned 行政关闭,但缺陷未确认修复,重开需现行复现细节"。引用它们的团队应先查当前仓库状态,不要按本文口径当作已解决。

总结

审批网关的信任模型有个容易被忽略的前提:它假设被管辖的主体碰不到管辖规则本身。Hermes 案证明这个前提在真实产品里会被打破——拒绝消息顺手教了绕过;OpenClaw 案则从另一侧补了一刀:格式化函数顺手把连接凭据漏进了模型可见的输出,说明"主体碰不到不该碰的东西"这条更宽的边界同样没守住。

而 #59293 的后续讨论显示,同一族前门不止一扇:有实测报告把 hermes plugins disable <guard-plugin>、hermes config set plugins.enabled、hermes config set command_allowlist 列为同类未设防路径,还确认了一条"永久允许项短路无人值守 deny"的覆盖关系。对防守侧来说,这不要求读懂每个项目的代码,只要求换一个问题去审计自己的 agent 环境:如果里面那个程序今天想干一件被你禁止的事,它手上现有的全部合法工具加起来,够不够让它自己放行? 答案若是"够",网关就只是个日志装饰。

本文面向授权安全研究与防御建设,未经授权对任何系统实施文中技术属违法行为。

参考来源

  • NousResearch/hermes-agent issue #104059(审批网关自绕过生产事故报告,https://github.com/NousResearch/hermes-agent/issues/104059 ,采集于 2026-11-05)
  • NousResearch/hermes-agent issue #59293(config CLI 绕过写保护跟踪 issue 及修复讨论,https://github.com/NousResearch/hermes-agent/issues/59293 ,采集于 2026-11-05)
  • openclaw/openclaw issue #158966(凭据化 CDP WebSocket URL 进入模型可见输出,https://github.com/openclaw/openclaw/issues/158966 ,采集于 2026-11-05)
  • openclaw/openclaw Security Advisories 列表页(https://github.com/openclaw/openclaw/security/advisories ,采集于 2026-11-05)