一个插件市场审核了某个提交,你的编程代理装完报告"安装成功",实际跑的却是另一个提交的代码。唯一的痕迹是 git 抛出的一句警告:refname is ambiguous(引用名有歧义)。
这就是 Plugin4Shell。它由 Air Security 的 Or Nevo、Dor Granat 和 Niv Hoffman 于 2026 年 9 月 17 日公开,波及 Claude Code、Codex、GitHub Copilot 和 Gemini CLI 的插件安装器。Air 把根因称为"少了一次检查":代理检出了被固定的提交,却从没验证自己真的落在了那个提交上。
一串 40 位的十六进制字符,看起来只能指向一个东西。事实并非如此——分支可以叫这个名字,而攻击用的正是这一点。
安装器到底跑了什么
Air 的报告中给出了命令。Claude Code、Codex 和 Copilot 走的是:先 git clone,再 git checkout 那个提交号。Gemini CLI 则是先浅克隆,再 git fetch origin,最后 git checkout FETCH_HEAD。
攻击链条是这样的:
- 先发布一个无害插件,通过审核,市场把提交 aaa… 固定下来,用户装的就是这个被审核的版本。
- 再推一个无害更新,市场重新固定到 bbb…。
- 创建一个名为 bbb… 的分支,指向恶意代码,并把它设为仓库的默认分支。被审核的那个提交仍留在仓库里,没被动过。
- 固定值变化触发后台插件更新,git 把这个名字解析成了分支,代码在没有任何提示的情况下被执行。
Gemini CLI 的变体只是把第四步换成了默认分支名叫 FETCH_HEAD。固定值还在,只是压在它上面的名字指向了别处。
为什么分支赢了
git 对同一串字符有两种读法,取决于命令。
git rev-parse 走的是 object-name.c 里的 get_oid_basic。当输入恰好是 40 位十六进制时,它返回那个对象 ID。即使存在同名引用,它也只是打印警告,仍然返回对象——提交赢。
git checkout 走的是 builtin/checkout.c 里的 setup_new_branch_info_and_source_tree。它解析参数后,检查 refs/heads/ 下是否存在同名引用。如果存在,就用分支的提交替换掉那个提交,并切换到该分支——分支赢。
git 自己的提示文本甚至写着,这类引用"在你只指定 40 位十六进制时会被忽略"。这句话对版本查找成立,对检出并不成立。同样的 40 个字符,两个答案:rev-parse 拿到提交,checkout 拿到分支。
分支还必须是默认分支,原因在于 git clone 的工作方式。克隆只为远端的默认分支创建本地分支,其他分支都以 refs/remotes/origin/ 下的远程跟踪引用形式存在,checkout 找不到同名本地分支,就会退回提交。Air 确认,非默认分支无法完成这次攻击。
Gemini CLI 那条路径以同样的方式失败,只是名字不同。FETCH_HEAD 通常是 git fetch 写在 .git/ 里的一个文件,记录刚取回的内容。如果克隆的默认分支就叫 FETCH_HEAD,同名本地分支存在,checkout 会优先选它,取回的固定提交永远到不了磁盘。换个引用名并不是防御手段,真正有效的是在检出之后检查落地的到底是什么。
影响范围比标题小
GitHub 会拦截这种分支名。其文档称不允许看起来像 Git 对象 ID 的名字,即 40 位 0-9 和 A-F 组成的字符串。一位 GitHub 发言人告诉 The Register,这"确保所报告的漏洞无法在 GitHub 上被利用"。GitLab 在推送时也有类似检查。
其他托管方没有这道防线。Air 称 Bitbucket 和自建 git 服务器接受 SHA 形状的分支名。Claude Code 的文档把 GitLab、Bitbucket、GitHub Enterprise Server 或其他 git 托管方列为可以托管插件市场的地方。
FETCH_HEAD 是否被覆盖并不明确。The Hacker News 指出,GitHub 的规则"没有明确拦截这个名字"。
零点击取决于自动更新。Air 称 Claude Code 和 Codex 默认在后台更新已安装插件。Anthropic 的文档口径更窄:对第三方市场,后台自动更新处于关闭状态,直到用户或管理员开启。The Hacker News 对 Anthropic 和 GitHub 文档的解读一致:只有各代理自带的市场默认开启,外部市场则是关闭或可选。
目前还没有 CVE。截至 9 月 18 日,The Hacker News 没有发现任何 CVE,四家厂商也没有发布公告,没有迹象显示该漏洞已在真实攻击中被使用。
所以现实中的目标群体是:使用自建或 Bitbucket 市场、开着自动更新的团队,或者任何手动从这类市场更新插件的人。这比标题暗示的范围小,但这群人恰恰最可能运营私有市场。
还有一个问题悬着。The Hacker News 指出,没有人说明更新代理时,是会移除已经被掉包的插件,还是只拦截未来的掉包。
修复方案与各家的进度
Air 建议的检查只有一行,在检出之后运行:test "$(git rev-parse HEAD)" = "$PIN" || exit 1。它比对落地的东西和获批的东西,不管 git 怎么理解那个名字。
OpenAI 的补丁 PR #34644"Verify Git plugin SHA checkouts"于 2026 年 7 月 22 日合并,在 Codex 里做了同样的事。其描述写道:"当远端的默认分支同名时,Git 可能把请求的提交 SHA 解释为分支名。"它在检出后解析 HEAD,不匹配就拒绝该来源。该修复随 Codex 0.146.0 发布。
各家现状:Gemini CLI 已被弃用但仍在运行。Google 于 2026 年 6 月 18 日停止向 Google AI Pro、Ultra 和免费个人账户提供它,它仍可通过付费 API 密钥以及 Gemini Code Assist 标准版或企业版使用,而这些安装就是漏洞的留存处。Google 引导用户转向 Antigravity,据 TNW 报道,它没有可被绕过的插件固定机制。
如果你同时跑多个代理,就更新每一个 CLI。在终端里:claude --version 看是否从 2.1.179 起修复,codex --version 看是否从 0.146.0 起修复,然后 claude update、npm install -g @openai/codex@latest。Codex 如果不是用 npm 装的,就按原来的方式更新。Copilot 的插件源留在 GitHub 上,那里推不了 SHA 命名的分支。Gemini CLI 方面,GitHub 的规则没有明确覆盖 FETCH_HEAD 分支,如果还在用就迁走。
热门跟贴