那个 commit 是一条分支
Air 在 9 月 17 日披露了 Plugin4Shell:Claude Code、Codex、Gemini CLI 和 Copilot 的插件市场,都把每个插件钉在一个四十字符的 commit 哈希上,而智能体会把那个哈希检出,却从不确认自己拿到的就是它。攻击者只要把一条分支起成被钉住的哈希那个名字,就能赢得检出,剩下的由后台更新程序完成,没有提示,也没有点击。Anthropic 和 OpenAI 已修补。Google 表示不会修,Microsoft 至今没有动作。在 Bromure Agentic Coding 工作区里,赢得检出的代码落进的是一台只有诱饵凭证的 VM,外面还有由宿主机强制执行的推送策略和出口规则。
每一份供应链指南都告诉你,把依赖钉在哈希上,因为版本号只是一张别人搬得动的标签, 而哈希就是内容本身。四个编码智能体听从了这个建议。没有一个去确认,拿到的哈希是不 是它要的那一个。
一个插件通过了评审。你读过它,或者你的平台团队读过它,于是它钉在某个 commit 上进了 核准清单:四十个十六进制字符,只能描述唯一一棵树,因为改动树就会改动字符。这正是它 的吸引力。任何握有合适权限的人都能挪动一个标签或重新发布一个版本,但 commit 哈希 本应是一项事实。
9 月 17 日,Air 的三位研究员(Or Nevo、Dor Granat 和 Niv Hoffman)发表了 Plugin4Shell。他们的结论是:钉选 的价值不会高于解析它的那段程序,而四个主要编码智能体的解析,并不检查自己的成果。 The Register 当天就做了报道。 Claude Code、OpenAI 的 Codex、Google 的 Gemini CLI 和 Microsoft 的 Copilot 全都 受影响。
git 偏爱 ref
commit 哈希是内容,同时也是一个名字。分支和标签也是,而 git 用同一套查找把它们 全都解析出来。你交给 git 一个既是分支名又是对象 id 的字符串,它就得挑一个;它挑 分支。研究员用一句话讲完:“当一个名字同时是有效的 ref 和对象 id 时,git 偏爱 ref。”
git 确实会警告。它会为那个引用打印一条歧义提示,打印进一个你并没有在看的终端, 因为执行检出的是一支后台更新程序。
git 的分支名接受四十个十六进制字符。控制插件仓库的攻击者,建立一条与市场钉选同名的 分支,并把它设为默认分支。克隆时,这条分支会跟着同名的 commit 一起下来。接着智能体 执行它的检出:
git clone <plugin repo> ./
git checkout aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
检出落在分支上。Gemini CLI 走另一条路到同一个地方:它用 --ref 取得正确的 commit,
然后执行 git checkout FETCH_HEAD,而只要仓库里有一条叫 FETCH_HEAD 的分支,
这一行就会解析到那条分支。
五个步骤,最后一个没有点击
研究员把攻击拆成五个动作,而前三个都是一个运作良好的市场刻意在做的事。
埋下。 发布一个良性插件。它在 commit aaa…aaa 通过评审,因为它确实没有问题。
采用。 开发者安装它,钉在已评审的 commit 上。这一步,你做的正是指南要你做的事。
版本更新。 市场把钉选换到较新的 commit bbb…bbb,同样良性,同样已评审。
抽地毯。 攻击者建立一条叫 bbb…bbb 的分支,设为默认,并让它指向恶意代码。他
什么都没有重新发布,市场的记录也没有移动。你会去核对的那四十个字符,就是你先前
核对过的那四十个字符。
从自动更新到远程代码执行。 Claude Code 和 Codex 默认在后台更新已安装的插件。 下一次排定的更新就会检出那条分支。Air 把这一步形容为“没有提示,没有点击”。
还有第二条门路,可以跳过埋下这一步:接管一位正当插件作者的仓库,把恶意版本推给每 一个已经装了该插件的智能体。分支的把戏一模一样,而且没有评审需要通过。
Air 把结果描述为“在员工机器上的完整远程代码执行”,以该员工的权限执行。攻击者不需要 提权。代码以笔记本主人的身份执行,触及他们的文件与他们的生产环境访问权。
这已经是这几位研究员第三次数到同一个形状。他们先前关于恶意智能体技能的研究,描述过 一个触及超过 26,000 个智能体的技能,以及一场劫持行动,拿下了已在 134,000 个智能体 上使用的 925 个技能。那些行动需要你安装坏东西; 这一次需要你安装好东西,然后等着。
四家里有两家没有补丁
Air 在 6 月通报了四家厂商。
Anthropic 在 6 月 17 日修补了 Claude Code,版本 2.1.179。OpenAI 的修正随 Codex 0.146.0 发布,并在 8 月 12 日获得验证。Google 在 8 月 4 日回复,Gemini CLI 已经 弃用,不会修补。Google 让用户改用 Antigravity,所以一份留在磁盘上的 Gemini CLI 就一直留着漏洞。Microsoft 没有为 Copilot 发布任何东西。GitHub 表示自家平台会挡下 看起来像 commit SHA 的分支与标签名,这覆盖了托管在 GitHub 上的插件,对托管在别处 的插件仓库则毫无作用,Bitbucket 也一样。The Register 引用 Microsoft 自己的数字 指出,财富五百强企业中有将近 90% 在用 Copilot。
修正本身只有一行,而且必须跑在智能体内部:
test "$(git rev-parse HEAD)" = "<pinned-sha>" || abort
解析出落进工作区的那个 commit,除非相符否则拒绝继续。没有任何市场能代你做这件事, 因为在市场把话说完之后,是你的智能体在你的机器上解析那个钉选。
对今天读到这篇文章的大量开发者来说,“更新你的智能体”不是一个可用的答案。那就剩下 另一个问题:当那段代码赢得检出并开始执行,它会找到什么?
在一个 Bromure Agentic Coding 工作区里
Air 那句话,以员工的权限执行,说得很准。工作区存在的意义,就是让它不成立。
在 Bromure Agentic Coding 里,智能体跑在你 Mac 上一台硬件虚拟化
的 Linux VM 中,而这台 VM 从来不持有真正的凭证。会话启动时,应用会把保留结构的诱饵
写进智能体运行的环境:ANTHROPIC_API_KEY、GH_TOKEN、LINEAR_API_KEY 等等作为
环境变量,并把相对应的假值写进 ~/.git-credentials、~/.docker/config.json、
~/.kube/config、~/.aws/config 以及智能体的 MCP 配置文件。每个诱饵都保留工具预期
的形状,所以工具会接受它:GitHub 诱饵是 ghp_ 加 36 个字符,Anthropic 的以
sk-ant-api03-brm- 开头,Kubernetes 的 bearer token 以 brm-k8s- 开头。Bromure 以
每次安装专属的盐,通过 HKDF-SHA256 从真实值派生出每一个诱饵,因此它跨会话稳定不变,
而对捡走它的人毫无价值。
真实的值加密存放在宿主机上。宿主机端的代理在请求离开 VM 之后,才在链路上把它们换 回去,而且只在目的地符合该凭证所签发的主机时才换。一个插件扫过家目录寻找配置文件, 这正是这类载荷第一件会做的事,它捡到的只是一组格式完好的诱饵。
推送必须在宿主机上获得批准
一个被入侵的插件在开发者机器上能做的最有价值的事,就是推送。 一台笔记本变成一百个仓库,正是这样发生的。
Guardrails 会把智能体发往 GitHub 的每一次调用分类。走 HTTPS 的 git push 到达代理
时是 git-receive-pack,算作写入。只读模式拒绝它。写入前先询问模式会在你的
Mac 上弹出一个对话框,标题是 Allow write on "<scope>" from workspace "<name>"?,
一字不改地显示该操作,而不是它的摘要。git fetch 算作读取,不打扰你就通过。
这个分类跑在宿主机的代理里,在 VM 之外。来宾内部的代码既关不掉它也绕不过去,因为它 不是跑在做决定的那台机器上。
对外的呼叫撞上一条来宾改不了的规则
每个工作区都有一道出口防火墙:一份有序的、pf 风格的允许与拒绝规则清单,涵盖主机、
IP 范围、协议、端口,而对网页流量还涵盖单个的 HTTP 动词。
allow web api.example.com GET,POST 让智能体只能查询、不能变更,就在链路上生效,
无论 VM 里哪个工具发出请求都一样。
有两层在执行它。虚拟网络交换机依目的 IP 和从 DNS 侧录到的主机名,对每一条流套用 规则,除了 HTTPS 也涵盖纯 TCP 与 UDP。代理再依 TLS 服务器名与 HTTP 方法套用一次。 Bromure 不需要来宾配合,就把 VM 的 80 和 443 端口流量导进代理,所以里面没有任何东西 能豁免于检查。你的修改会立刻送达进行中的会话,这在你读到像这样一则披露的当天特别 重要。
底下还有失陷检测器,默认开启,没有东西要配置。代理会在单次扫描中,检查每个对外请求 里有没有为该工作区铸造的诱饵令牌。一个诱饵被寄往它从未被铸造去对应的主机,就是外泄 的特征,代理会以 HTTP 451 回应,一个字节也不转发。它当场暂停 VM,并发出告警,让你 选择关闭工作区、导出磁盘与家目录以供调查,或是自担风险继续。Bromure 会把该工作区 标记为已失陷,下一次启动会清除 VM 磁盘与家目录,同时保留你的令牌、SSH 密钥与工作区 设置。走掉的只有诱饵,所以没有什么需要轮换。
零点击意味着没有东西可供察觉
步骤五最锐利的性质是它的安静。后台的插件更新不会产生提示,也不会产生工具调用,所以 智能体自己的界面,没能给开发者任何可以抓住的东西。Air 的论点是,这个攻击跑在系统中 你没有理由去盯着的那一块。
工作区把盯着这件事搬到别处。安全引擎把每一个决定写成你 Mac 上 Security Timeline
窗口里的一行:防火墙判决(evil.example:443 tcp — blocked)、Guardrails 的执行
(DELETE api.github.com/repos/… — blocked)、credential brokering,包括红色的
blocked — exfiltration attempt, VM paused 那一行,外加供应链判决与提示注入命中,
都有颜色标示、可以筛选。一个会话还可以录下已执行命令与已连到主机的加密轨迹。
一个没告诉你就自己更新的插件,终究还是得发出请求,而每一个请求都要越过一道边界, 那道边界在 hypervisor 的另一侧留着自己的记录。
先确认你的智能体版本
Claude Code 2.1.179 与 Codex 0.146.0 含有这项修正。如果你用 Gemini CLI,不会有 修正。如果你用 Copilot,目前还没有。
能关的地方就关掉插件自动更新
零点击这个性质来自后台更新程序。手动更新会把人重新放回步骤五的回路里。
在 CI 里自己验证检出
在钉选的检出之后执行 git rev-parse HEAD,拿来和钉选比对,就是厂商不得不补上的
那一行检查。凡是你用 SHA 克隆的地方,它都管用。
假设有人会赢得检出
钉选、评审、市场审核,在这里全都守住了,攻击还是打进来。接下来的结果,就取决于 赢得检出的那段代码够得着什么。
Plugin4Shell 用同样的条件抓住谨慎的开发者和粗心的开发者。装了当红热门那位,下场并不 比读了源码、钉住哈希、把钉选记下来的那位更糟。内容寻址本该保护第二位开发者,它却 没有做到,因为 git 是通过一种查找来解析这个地址,而那种查找也把同样的字符拿去指别的 东西。
继续钉选。然后假设:一个建立在查找或名称匹配之上的控制,总有一天会在你睡着的时候被 交上错误的答案。为那个早晨做设计:让智能体跑在一个没有任何值得拿走的密钥的地方, 并把它做过什么的记录,留在代码够不着的边界另一侧。