四个编程智能体都把插件钉在一个 40 字符的 commit SHA 上,却没有一个去核对自己拿到了什么。2026 年 9 月 17 日公布的 Plugin4Shell 不是一个精巧的利用手法——它是这样一个发现:业界标准的那道供应链控制,被实现成了一个请求而不是一次检查,而且在 Claude Code、Codex、Copilot 与 Gemini CLI 里同时如此。真正耐久的教训在厂商们的应对里:两家补上了那次缺失的比对,两家指向了自己 git 托管方的命名规则——当客户端不做校验时,安全性质就住在注册中心那边,你的锁文件看不见它,而它经不起一次镜像。
一览
一处遗漏、四个产品、四种关于「这该是谁的活」的理论。
| 智能体 | 清单钉住了什么 | 它校验了什么 | 应对 |
|---|---|---|---|
| Claude Code | 一个 40 位十六进制 commit SHA | 修复之前:什么也没校验 | 在 2.1.179 修复 |
| OpenAI Codex | 一个 40 位十六进制 commit SHA | 修复之前:什么也没校验 | 在 0.146.0 修复 |
| GitHub Copilot | 一个 40 位十六进制 commit SHA | 什么也没校验 | 客户端无修复;依赖 GitHub 的引用命名规则 |
| Gemini CLI | 一个 40 位十六进制 commit SHA | 什么也没校验 | 无修复;产品已弃用,转向 Antigravity |
究竟发生了什么
机制
智能体的插件市场会把每个条目钉在一个 commit 哈希上——一串 40 位十六进制字符,指名一个仓库的某个确切快照。智能体克隆插件仓库,然后检出这串字符。按 AIR Security 的披露(署名研究者 Or Nevo、Dor Granat 与 Niv Hoffman),四个智能体之后都没有问过 git:自己究竟落在了哪个 commit 上。
这件事之所以要紧,是因为一串 40 位十六进制字符不只是一个对象 ID,它同时也是一个语法上合法的引用名。git 自己的版本解析,在遇到一个有歧义的名字时会优先采用匹配它的引用,并就此发出警告。于是,仓库所有者只要创建一个名字恰好等于那个被钉住 SHA 的分支——并把它设成仓库的默认分支——就能向一个「请求了这个钉、却从不确认它」的客户端送出完全不同的代码。清单条目没变。批准这个插件的那次评审没变。用户什么都不用点。
修法是一次比对,而它的缺席就是整个漏洞:
# what the agents did git clone --depth 1 "$REPO" plugin git -C plugin checkout "$PINNED_SHA" # what makes the pin binding — one line, no network call test "$(git -C plugin rev-parse HEAD)" = "$PINNED_SHA" || exit 1
为什么是零点击,以及为什么严重
编程智能体的插件与扩展不是被沙箱隔开的边车。它们以运行该智能体的开发者的权限运行,在现实中这意味着工作区、SSH 密钥、环境里的云凭据、内部包注册中心,以及那台笔记本拥有的一切生产访问权。而由于替换发生在更新而不是安装时,一个已批准的插件会变成攻击者控制的插件——没有弹窗、不用重装,也没有任何 diff 给人看。
有意思的是那两家没打补丁的厂商
很容易把这次分裂的应对读成「两家负责、两家懈怠」。这种读法是错的,而正确的读法更让人不安。
GitHub 的立场是:其托管仓库上的命名限制压缩了攻击面——而这在事实层面是对的。GitHub 会拒绝任何分支或标签名为 40 位十六进制字符的推送,还专门配了一条错误信息:GH002: Sorry, branch or tag names consisting of 40 hex characters are not allowed。GitLab 也在 pre-receive 钩子里拦掉同样的形状。git 本身长期以来就避免创建这类引用,原因正是那种歧义。在这些平台上,攻击在开始之前就被拦住了。
所以对披露之前的世界,诚实的描述不是「四个坏掉的客户端」。而是:一条所有人都在依赖、却没人实现过、并且事实上是由两家 git 托管方作为易用性防护免费提供的安全性质。在它成立时,这是个不错的结果。问题在于那些会把制品从它底下搬走的一切。
什么会打破托管方的保障
- 自托管的代码托管服务。Gitea、Forgejo 或一个纯裸仓库远端不一定执行这条规则,而内部插件仓库恰恰就住在这些地方。
- 版本更旧或配置不同的企业实例。这个检查是一项平台策略,不是协议规则;一个没跟上升级的实例,是在同一份清单之下的另一种安全姿态。
- 镜像与内嵌。离网重新托管、透传缓存、并购迁移,都会把仓库搬到规则不同的命名空间里,而没有谁的迁移检查清单会提到引用命名策略。
- 产品决策。这条规则之所以存在,是因为一家公司这么选了;它被写成一项便利措施,并且可以在没有任何 CVE 的情况下被放宽。
顺着那几列读下去,那种不对称就是全部要点。客户端一侧的比对便宜、本地、不需要任何人配合,而且会出现在代码评审里。托管方一侧的禁止在适用之处很强,却在你交付出去的那件东西里完全不可见:无论规则是否在生效,你的插件清单看起来一模一样,所以任何审计、diff 或 SBOM 都说不出你身处哪个世界。
这周该改什么
- 先升级,然后按托管方清点。Claude Code 2.1.179 与 Codex 0.146.0 关掉了这个洞。对仍在依赖平台的那些,问题不是「我们打补丁了吗」,而是「我们的插件仓库住在哪儿」——按托管方把它们列出来,并标出每一个不在执行该规则的平台上的仓库。
- 凡是你控制着「取」的地方,就自己在取完之后校验。如果你的团队通过任何形式的安装器分发内部插件、技能或提示词包,请加上那次
rev-parse HEAD比对。四家厂商都漏了它;你那份两百行的安装脚本几乎肯定也漏了。 - 别再把版本号当成钉。一条有版本、没哈希的锁文件条目是一个请求。请优先选择「本身就是内容」的标识符——摘要,而不是名字;在只有名字可用的地方,请在第一次取用时记下解析出的摘要,并在此后每一次取用时比对。
- 把 Markdown 当代码来评审。技能、插件说明与提示词包会操纵工具调用,所以对其中之一的一次静默改动就是一次权限改动。请把它们放进和依赖升级同一套审批里;关于为什么文件扩展名会骗人,见智能体技能。
- 缩小一个插件够得着的范围。这件事之所以是严重而不是恼人,是因为插件继承了开发者拥有的一切。一个在容器里、配着受限凭据运行的编程智能体,比一个在带着生产密钥的笔记本上运行的,是小得多的一次事故——这就是影响半径的论点,出现在它最常被跳过的那个地方。
越过插件之后仍然成立的部分
把 git 的细节剥掉,Plugin4Shell 是一个你会反复遇到的形状,因为智能体把依赖图延伸进了经典打包体系从未覆盖的地方。模型版本靠别名取。工具 schema 在每一次 MCP 连接时重新送来,而它们的描述是你提示词的一部分。技能与提示词包,是按名字从几个月前建起的注册中心里拉的。这些几乎都没有锁文件,而凡是有「钉」的地方,那个钉通常是一个要经过别人拥有的命名空间才能解析的名字。
在设计评审里抓住这一类,只需要一句话的测试:我们能不能靠自己察觉一次替换——不发网络请求、不信任服务器?如果答案是能,你手上有摘要也有检查。如果答案是不能,你手上只有一个偏好——而且你已经悄悄把自己供应链的安全性,委派给了一家从未做出此承诺的公司。一般形式见钉版本与校验,其余的攻击面见智能体供应链安全。
常见问题
我的插件全都在 GitHub 上,是不是就安全了?
就这个具体的手法而言,大体上是——GitHub 会直接拒绝 40 位十六进制的引用名。但那是一项平台策略在保护一个不做校验的客户端,所以它只覆盖执行该规则的平台上的仓库,而且在插件被镜像、被内嵌或被搬到内部代码托管服务的那一刻,就不再覆盖你。
这个漏洞在野被利用过吗?
目前没有公开证据显示已被利用。注意这次攻击需要什么:控制一个别人已经批准过的插件仓库——而站在那个位置上,你本来就可以直接推一个恶意 commit。绕过钉这件事,让攻击对评审不可见,而不是让攻击成为可能。
Google 为什么弃用 Gemini CLI 而不是修它?
转向 Antigravity 的弃用本来就在推进中;披露落在了一个正在退役的产品上。在运维层面这个区分帮不上你——一个没打补丁的客户端留在开发者机器上就是没打补丁,与路线图无关,所以请把这次弃用当作一条升级截止期。
按 SHA 钉版本还有意义吗?
有,而且比以前更有意义。commit ID 是一个由内容推导出来的标识符,恰恰是正确的那一类——失效的是那次缺失的比对,不是那个标识符。请钉住 SHA,然后去核对那个 SHA。
我怎么审自己的安装器有没有同一个 bug?
把你所有「按某个标识符去检出、下载或解包某样东西」的地方 grep 出来,看紧接着有没有那次比对。要找的模式是:一次取用,其结果在没有哈希检查的情况下就被使用了——shell 脚本、CI 任务、Dockerfile 和插件加载器,一视同仁。
延伸阅读
本站相关:
- 钉版本与校验 —— 名字与摘要之别,以及为何只有一种能在本地被核对。
- 智能体供应链安全 —— 这件事所处的完整攻击面。
- 智能体技能 —— 为什么一个 Markdown 包就是代码。
- 工具目录生命周期 —— 智能体所加载之物的接入、评审与下架。
- MCP 注册与分发 —— 同一个信任问题,往上挪一层。
来源:
- AIR Security —— Plugin4Shell,原始披露。
- Help Net Security 关于受影响版本与厂商应对。
- gitrevisions —— git 如何解析一个有歧义的 40 位十六进制名字。