1 · 30 秒速览1 · The 30-Second Version
2026 年 9 月 17 日,以色列安全公司 AIR 公布了一个叫 Plugin4Shell 的漏洞。受影响的是四款主流 AI 编码 Agent:Claude Code、OpenAI Codex、GitHub Copilot、Gemini CLI。
On 17 September 2026, Israeli security firm AIR disclosed a vulnerability they named Plugin4Shell. It affects four mainstream AI coding agents: Claude Code, OpenAI Codex, GitHub Copilot, and Gemini CLI.
先说它为什么值得你花二十分钟:这不是「有人装了来路不明的插件所以中招」。受害者做的每一步都是对的——从可信的 marketplace 安装、插件经过审查、审查后钉扎到一个具体的 commit。这套流程正是行业推荐的做法。Plugin4Shell 攻击的就是这套流程本身。
Why it is worth twenty minutes: this is not "someone installed a sketchy plugin and got burned." The victim did everything right — installed from a marketplace they trust, from a plugin that passed review, pinned to a specific reviewed commit afterwards. That is exactly the process the industry recommends. Plugin4Shell attacks the process itself.
Plugin4Shell 是一个「插件 SHA 钉扎绕过」漏洞:Agent 按 marketplace 钉扎的那个 commit SHA 执行 git checkout,但从不校验签出之后工作区真的停在那个 commit 上。控制了插件仓库的攻击者,可以让这次签出解析到自己的恶意代码,而钉扎在外部看起来完全正常。结果是在四款 Agent 上都能拿到零点击的远程代码执行。
Plugin4Shell is a plugin SHA-pinning bypass: the agent runs git checkout against the commit SHA the marketplace pinned, but never verifies that the working tree actually landed on that commit. An attacker who controls the plugin's repository can make that checkout resolve to malicious code while the pin still looks honored from the outside. The result is zero-click remote code execution across all four agents.
Claude Code:Anthropic 已在 2.1.179 修复(AIR 记录的确认日期为 2026-06-17)。Codex:OpenAI 在 0.146.0 修复(2026-08-12 验证)。GitHub Copilot:微软收到同样的披露,至今未发布补丁。Gemini CLI:Google 于 2026-08-04 确认不会修复——该产品已弃用,官方建议迁移到 Antigravity。
Claude Code: Anthropic patched it in 2.1.179 (AIR records the confirmation date as 2026-06-17). Codex: OpenAI patched in 0.146.0, verified 2026-08-12. GitHub Copilot: Microsoft received the same disclosure and has shipped no fix. Gemini CLI: on 2026-08-04 Google confirmed it will not be patched — the product is deprecated, and users are told to migrate to Antigravity.
2 · 背景:add-on 怎么变成了攻击面2 · Background: How Add-ons Became the Attack Surface
要看懂 Plugin4Shell 为什么是个转折点,得先明白它之前发生了什么。AIR 自己把这称为「第三幕」,前两幕都是他们做的。
To see why Plugin4Shell is a turning point, you need what came before it. AIR calls this "the third act" of a story — and they wrote the first two acts themselves.
大约一年前,编码 Agent 还是个封闭的东西:一个 CLI,一个模型,你的代码。现在它是一个插件宿主——skill、plugin、MCP server、hook,全都从第三方仓库装进来,并且以你的身份、在你的机器上、带着你的凭据运行。安全边界就此从「模型会不会说错话」移到了「这些东西是从哪来的」。
A year ago a coding agent was a closed thing: one CLI, one model, your code. Today it is a plugin host — skills, plugins, MCP servers, hooks, all pulled in from third-party repositories and running as you, on your machine, with your credentials. The security boundary moved from "might the model say something wrong" to "where did all of this come from."
行业对前三幕给出的答案就是 SHA 钉扎:审查某一个 commit 的代码,把这个 commit 钉住,之后跑的永远是这个 commit。听起来无懈可击——commit SHA 是内容哈希,改一个字节哈希就变了,这是 git 的地基。
The industry's answer to those first three acts was SHA pinning: review the code at one commit, pin that commit, and trust that the pinned commit is what runs forever after. It sounds airtight — a commit SHA is a content hash, change one byte and the hash changes. That is bedrock git.
Plugin4Shell 的洞见是:问题从来不在哈希本身,而在「把那串字符交给 git 时,git 会拿它去找什么」。
Plugin4Shell's insight is that the hash was never the weak part — the weak part is what git goes looking for when you hand it that string.
3 · 机制:git 为什么会被骗3 · Mechanism: How git Gets Fooled
先讲清一件 git 的事:ref 优先于 object idOne git fact first: refs beat object ids
object id 就是那串 40 位十六进制的 commit SHA,它直接指向仓库里的一个对象。ref 是「人类起的名字」——分支名、tag 名都是 ref,它是一个指向某个 object id 的可变指针。你敲 git checkout main 用的是 ref,敲 git checkout a1b2c3… 用的是 object id。
An object id is that 40-character hex commit SHA; it points directly at an object in the repository. A ref is a human-given name — branch names and tags are refs, each a mutable pointer to some object id. git checkout main uses a ref; git checkout a1b2c3… uses an object id.
关键问题:如果一个名字同时是合法的 ref 和合法的 object id,git 选哪个?
The key question: if a name is both a valid ref and a valid object id, which does git pick?
git 选 ref,并且只打印一行 warning: refname '…' is ambiguous——一句警告,然后照常执行。这不是 bug,是 git 长期以来的既定行为(GitLab、Azure Pipelines 等项目的 issue 里都有关于这个歧义解析的记录)。
git picks the ref, printing a single warning: refname '…' is ambiguous line and then carrying on. This is not a bug; it is long-standing documented git behaviour (issues about this ambiguity exist in GitLab, Azure Pipelines and other projects).
那么下一个问题自然是:分支名能叫成 40 位十六进制吗?git 自己的 git check-ref-format 是接受的,遵守协议的托管方也接受。GitHub 是例外——它直接拒绝 40 位十六进制的分支名。但 Bitbucket 和任何自建 git 服务器都允许,而 Anthropic 官方文档明确把 Bitbucket 和自建 git 列为合法的 marketplace 后端。
Which raises the next question: can a branch be named as 40 hex characters? git's own git check-ref-format accepts it, and hosts that follow the protocol accept it too. GitHub is the exception — it rejects 40-hex branch names outright. But Bitbucket and any self-hosted git server allow them, and Anthropic's own documentation lists Bitbucket and self-hosted git as valid marketplace backends.
变体一:Claude Code / Codex / CopilotVariant one: Claude Code / Codex / Copilot
这三家的安装逻辑本质上是两行:
For these three, installation boils down to two lines:
# AIR 记录的安装序列
git clone <plugin repo> ./
git checkout aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
攻击者控制了上游仓库之后,建一个分支,名字就是那 40 位 SHA,并把它设为仓库默认分支。上面那句朴素的 git clone 于是把它作为本地分支拉下来;git checkout 解析那串字符时,ref 优先,签出的是分支。被钉扎的那个 commit 还在不在,已经完全不重要了。
Once the attacker controls the upstream repository, they create a branch whose name is exactly that 40-hex SHA and make it the repository's default branch. The plain git clone above then brings it down as a local branch of that name, and git checkout resolves the string to the ref rather than the object. Whether the pinned commit still exists stops mattering entirely.
① 托管方允许 40 位十六进制的分支名(GitHub 不允许,Bitbucket 和自建允许);② 该分支必须是仓库默认分支——非默认分支只会被拉成 remote-tracking ref,签出会回落到 commit 对象,攻击不成立。
① The host must allow 40-hex branch names (GitHub does not; Bitbucket and self-hosted do); ② the branch must be the repository's default — a non-default branch is only fetched as a remote-tracking ref, the checkout falls back to the commit, and the attack fails.
变体二:Gemini CLI 的 FETCH_HEADVariant two: Gemini CLI and FETCH_HEAD
Gemini CLI 用 --ref 钉扎,分三步安装:
Gemini CLI pins with --ref and installs in three steps:
git clone --depth 1 <plugin repo> ./
git fetch origin 41d0bc0a4aeb2fbf797dacea39e876d98c95024b
git checkout FETCH_HEAD
这里 git fetch 确实取到了正确的 commit,并把它记进 .git/FETCH_HEAD 文件。但 git checkout FETCH_HEAD 不一定会去读那个文件——如果仓库的默认分支名就叫 FETCH_HEAD,签出解析到的是那个分支,刚刚取回的 commit 被静默丢弃。
Here git fetch does retrieve the correct commit and records it in .git/FETCH_HEAD. But git checkout FETCH_HEAD does not have to read that file — if the repository's default branch is itself named FETCH_HEAD, the checkout resolves to the branch and the fetched commit is silently discarded.
换个说法:用寄快递打比方
你告诉快递员:「送到朝阳区幸福里 3 号」——这是一个精确地址(object id)。但小区物业允许住户把自家门牌改名成「朝阳区幸福里 3 号」。快递员的规矩是:先看有没有叫这个名字的门牌,有就送那儿,没有才按地址找。攻击者把自己家的门牌改成了那串地址,并且把它挂在小区大门口最显眼处(默认分支)。快递员嘟囔一句「这名字有点歧义」,然后把包裹送给了攻击者。
Another way to put it: a parcel delivery
You tell the courier: deliver to 3 Happiness Lane, Chaoyang — a precise address (the object id). But the building management lets any resident rename their own door plate to "3 Happiness Lane, Chaoyang." The courier's rule is: first look for a door plate with that name, and only fall back to the address if none exists. The attacker renames their own plate to that string and hangs it at the main gate where it is seen first (the default branch). The courier mutters "that name is a bit ambiguous," and hands over the parcel.
AIR 强调的一点值得单独记住:同一个设计错误出现在每一款受影响的 Agent 里。这不是某家公司写码时的疏忽,而是整个行业对「钉扎」这件事的理解都停在「把 SHA 传给 checkout」这一步,没人走到「确认 checkout 的结果」那一步。
One point AIR makes is worth remembering on its own: the same design error sits in every affected agent. This is not one company's coding slip — it is an entire industry's understanding of "pinning" stopping at "pass the SHA to checkout" and never reaching "verify what checkout produced."
4 · 为什么是「零点击」4 · Why It Is "Zero-Click"
到这里为止,你可能会想:那我不装新插件不就行了?
At this point you might think: fine, I just won't install anything new.
不行。让这个漏洞变成零点击的,是插件后台自动更新——AIR 指出,这在 Claude Code 和 Codex 里是默认行为。同一句 git checkout 会在后台更新时重新执行。所以当 marketplace 把钉扎的 SHA 往前推一版时,替换会送达已经装好的插件:没有安装步骤,没有提示框,没有任何可察觉的动静。
That does not help. What makes this zero-click is plugin background auto-update — which AIR notes is the default in Claude Code and Codex. The same git checkout re-runs on auto-update. So when the marketplace bumps the pinned SHA, the swap reaches already-installed plugins: no install step, no prompt, nothing to notice.
完整的攻击链是五步:
The full attack chain has five steps:
- 种下攻击者发布一个真的无害的插件到可信 marketplace,钉扎在 commit
aaa…,顺利通过审查。 - 被采用人们开始安装。每次安装都钉扎到
aaa…——那个被审查过、可信的版本。 - 版本更新攻击者发布一次常规更新,marketplace 重新钉扎到新 commit
bbb…。这个新版本仍然是无害的,审查照样通过。 - 抽梯子攻击者建一个名叫
bbb…的分支,设为默认分支,指向恶意代码。被钉扎的那个 commit 本身可以原封不动。 - 自动更新即 RCE钉扎值变了,触发每个 Agent 的后台自动更新。签出把
bbb…解析成分支,恶意代码落地运行。无提示,无点击。
- PlantThe attacker publishes a genuinely benign plugin to a trusted marketplace, pinned at commit
aaa…. It passes review. - AdoptionPeople install it. Every install is pinned to
aaa…— the reviewed, trusted version. - Version bumpThe attacker ships a routine update; the marketplace re-pins to a new commit
bbb…. That version is still benign and passes review too. - Rug-pullThe attacker creates a branch named
bbb…, sets it as the default, and points it at malicious code. The pinned commit itself can stay untouched. - Auto-update to RCEThe changed pin triggers every agent's background auto-update. The checkout resolves
bbb…to the branch, and the malicious code runs. No prompt, no click.
AIR 的原文值得直引其意:做对了事并不能保护你。那些走得比社区 marketplace 更远的组织——自己审查插件、自己钉扎到审查过的 commit——正是把 SHA 钉扎当作最后防线的人,而 Plugin4Shell 悄悄把这条防线抹掉了:审查通过、钉扎写入,装进来的却是另一份代码。所有建立在钉扎之上的下游审核流程,都继承了这次失败。
AIR's own framing is worth keeping: doing the right thing does not protect you. Organizations that go beyond a community marketplace — reviewing plugins themselves, pinning to a reviewed commit — are precisely the ones relying on SHA pinning as their safeguard, and Plugin4Shell quietly nullifies it: review passes, the pin is written, and different code installs. Every downstream vetting process built on pinning inherits the failure.
5 · 修复:一行断言5 · The Fix: One Assertion
两个变体,一个修法。签出之后,把工作区实际所在的 commit 解析出来,和钉扎值比对,不相等就中止:
Two variants, one fix. After checkout, resolve the commit the working tree is actually on, compare it to the pin, and abort on mismatch:
git checkout "$PINNED_SHA"
test "$(git rev-parse HEAD)" = "$PINNED_SHA" || abort
有两个细节决定了这行断言有没有用:
Two details decide whether that assertion actually works:
- 必须检查解析后的
HEAD,而不是「你请求的那个 ref」。这个区别正是 Gemini CLI 变体钻过去的地方——它请求的FETCH_HEAD名义上没错,错的是它解析到了什么。 - 必须跑在 Agent 内部。钉扎是在客户端解析的,所以任何 marketplace 都无法代为强制执行它自己宣传的这个保证。marketplace 最多能缓解分支名变体——只允许那些拒绝 SHA 形状分支名的托管方(实际上等于「只允许 GitHub」),但这会禁掉 Agent 官方支持的其他托管方,而且对 Gemini CLI 那个变体完全无效。
- It must check the resolved
HEAD, not the ref you asked for. That distinction is exactly what the Gemini CLI variant slips through — the requestedFETCH_HEADis nominally correct; what it resolved to is not. - It must run inside the agent. The pin is resolved on the client, so no marketplace can enforce the guarantee it advertises. A marketplace can at best blunt the branch-name variant by allowing only hosts that reject SHA-shaped names — effectively GitHub-only — but that bans hosts the agents officially support, and does nothing for the Gemini CLI variant.
| Agent | 状态 | 版本 / 日期 |
|---|---|---|
| Claude Code | 已修复 | 2.1.179(AIR 记录确认日 2026-06-17) |
| OpenAI Codex | 已修复 | 0.146.0(2026-08-12 验证) |
| GitHub Copilot | 未修复 | 微软已收到披露,截至本文调研时无补丁 |
| Gemini CLI | 不会修复 | 2026-08-04 Google 确认产品弃用,建议迁移 Antigravity |
| Agent | Status | Version / date |
|---|---|---|
| Claude Code | Patched | 2.1.179 (AIR records confirmation on 2026-06-17) |
| OpenAI Codex | Patched | 0.146.0 (verified 2026-08-12) |
| GitHub Copilot | Unpatched | Microsoft received the disclosure; no fix as of this research |
| Gemini CLI | Will not be fixed | On 2026-08-04 Google confirmed deprecation; migrate to Antigravity |
2026 年 5 月 · AIR 研究团队发现,并对四款 Agent 都做出可用 PoC → 2026 年 6 月 · 按协同披露流程通知四家厂商 → 2026-06-17 Anthropic 确认修复 → 2026-08-04 Google 确认不修 → 2026-08-12 Codex 验证修复 → 2026-09-17 公开披露(90 天窗口届满)。
May 2026 · found by AIR's research lab with working PoCs against all four agents → June 2026 · disclosed to all four vendors under coordinated disclosure → 2026-06-17 Anthropic confirms the fix → 2026-08-04 Google confirms no fix → 2026-08-12 Codex verified fixed → 2026-09-17 public disclosure (90-day window elapsed).
6 · 动手:把你自己的机器检一遍6 · Hands-On: Audit Your Own Machine
如果你用 Claude Code,升级就是完整的缓解措施。但升级只解决了「以后不会再被骗」,它不回答「我这台机器上现在装着什么」。三步:
If you use Claude Code, updating is the complete mitigation. But updating only answers "this can't fool me again" — it does not answer "what is on this machine right now." Three steps:
# 1. 确认版本(任何 ≥ 2.1.179 都已修;你大概率已在 2.1.27x)
claude --version
# 2. 盘点:装了什么、来自哪个 marketplace、钉在哪个 commit
# 用 JSON 输出,别靠肉眼读面板
claude plugin list --json | jq -r '.[] | "\(.name)\t\(.marketplace)\t\(.commit // "no-pin")"'
# 3. 两周没用过的插件,卸掉
claude plugin uninstall <name>
为什么第 3 步也算安全动作、而不只是省 token?因为受害面 = 你装着的插件数 × 每个插件仓库被接管的概率。第二个因子不是理论值:SkillJacking 已经证明了925 个在用的 skill 被从原维护者手里劫持;MCPJacking 在官方 marketplace 里找到 155 个依赖过期域名、可被直接注册接管的 MCP。每一个你不再使用却留着的插件,都是一条仍然开着的自动更新通道。
Why does step 3 count as a security action rather than just token thrift? Because your exposure = number of installed plugins × probability each plugin's repository gets taken over. That second factor is not theoretical: SkillJacking demonstrated 925 in-use skills hijacked from their original maintainers; MCPJacking found 155 MCPs in the official marketplace relying on expired domains, registerable by anyone. Every plugin you no longer use but still have installed is an auto-update channel left open.
团队与企业:把 marketplace 收口Teams and enterprises: lock down the marketplace
Claude Code 官方文档提供了 strictKnownMarketplaces:在托管设置(managed settings)里限制用户可以添加哪些插件 marketplace。因为它写在托管设置里,个人配置和项目配置都无法覆盖。空数组 = 完全锁死不允许任何第三方 marketplace;给一个列表 = 白名单;配 hostPattern 正则 = 域名级控制。常见做法是与 extraKnownMarketplaces 搭配——后者负责给全团队自动下发官方内部 marketplace,前者负责堵住其余一切。
Claude Code's documentation provides strictKnownMarketplaces: a managed settings key restricting which plugin marketplaces users may add. Because it lives in managed settings, neither user nor project configuration can override it. An empty array is full lockdown with no third-party marketplaces; a list is an allowlist; a hostPattern regex gives domain-level control. The common pairing is with extraKnownMarketplaces, which provisions your official internal marketplace team-wide while the former blocks everything else.
① 先升级(唯一的完整缓解);② 盘点并卸掉闲置插件(缩小受害面);③ 企业侧用 strictKnownMarketplaces 把 marketplace 收到白名单;④ 如果你自己运营 marketplace,优先选拒绝 SHA 形状分支名的托管方(GitHub 会拒),这不是完整防护,但能堵掉分支名变体。
① Update first (the only complete mitigation); ② inventory and uninstall idle plugins (shrink the exposure); ③ on the enterprise side, restrict marketplaces to an allowlist with strictKnownMarketplaces; ④ if you run your own marketplace, prefer a host that rejects SHA-shaped branch names (GitHub does) — not complete protection, but it closes the branch-name variant.
GitHub Copilot 至今没有补丁;Gemini CLI 被弃用、永远不会修。如果你的开发机、构建机或 CI 镜像里还留着它们,那不是「待打补丁」,是永久暴露——处理方式只能是卸载或替换,不是等。
GitHub Copilot has no patch; Gemini CLI is deprecated and will never get one. If either is still on a dev box, build machine or CI image, that is not "pending a patch" — it is permanent exposure, and the only response is removal or replacement, not waiting.
7 · 对比:传统包管理器早就解决了这个问题7 · Comparison: Package Managers Solved This Long Ago
这一节是概念对比,帮你把 Plugin4Shell 放进更大的背景里——下面的机制描述是各生态公认的设计事实,不是来自本次调研的某一篇来源,细节请以各自官方文档为准。
This section is a conceptual comparison to place Plugin4Shell in a larger context — the mechanisms described below are widely established design facts of each ecosystem rather than claims from a source fetched in this research; consult each ecosystem's own documentation for details.
| 生态 | 完整性保证的做法 | 校验发生在哪 |
|---|---|---|
| npm / yarn | lockfile 里记录 tarball 的 integrity 哈希,安装时对下载内容重新计算并比对 | 客户端,且真的比对了 |
| Go modules | 模块内容哈希写进 go.sum,并有公共 checksum database 做交叉见证 | 客户端 + 第三方公证 |
| 容器镜像 | 按 digest(内容哈希)拉取,运行时校验镜像层哈希 | 客户端,且真的比对了 |
| Agent 插件(修复前) | 记录 commit SHA,执行 git checkout | 没有比对这一步 |
| Ecosystem | How integrity is guaranteed | Where verification happens |
|---|---|---|
| npm / yarn | Tarball integrity hashes recorded in the lockfile; the downloaded content is re-hashed and compared on install | Client-side, and actually compared |
| Go modules | Module content hashes in go.sum, cross-witnessed by a public checksum database | Client-side plus third-party notarization |
| Container images | Pulled by digest (content hash), with layer hashes verified at runtime | Client-side, and actually compared |
| Agent plugins (pre-fix) | Record a commit SHA, run git checkout | The comparison step was missing |
看出模式了吗?其他生态都做了同一件事:拿到东西之后,重新算一遍哈希,和预期值比。Agent 插件生态借用了 git 的哈希,却把「比对」这一步交给了 git 本身——而 git 从来没承诺过「你给我一个 SHA,我一定给你那个 commit」。它承诺的是「我按我的解析规则找一个叫这个名字的东西」。误会就在这里。
See the pattern? Every other ecosystem does the same thing: after fetching, re-compute the hash and compare it to what you expected. The agent plugin ecosystem borrowed git's hashes but delegated the comparison step to git itself — and git never promised "give me a SHA and I'll give you that commit." It promised "I'll look up something by that name using my resolution rules." That is where the misunderstanding lives.
顺带一提:这个坑在别处出现过
「分支名伪装成 commit SHA」并不是 2026 年的新发明。安全研究者 Teddy Katz 在 2022 年就写过利用歧义 ref 从 GitHub Actions 里偷密钥的手法;GitLab 的 issue 追踪里也有「ambiguous branch name exploitation」的条目。换句话说,这是一个已知的 git 陷阱,只是在 2026 年被四个新生态同时踩了一遍。
An aside: this trap has appeared before
Branch names masquerading as commit SHAs are not a 2026 invention. Security researcher Teddy Katz wrote in 2022 about using ambiguous refs to steal secrets from GitHub Actions, and GitLab's issue tracker carries an "ambiguous branch name exploitation" entry. In other words, this is a known git trap — four new ecosystems just walked into it simultaneously in 2026.
8 · 坑与误解8 · Pitfalls and Misreadings
GitHub 确实拒绝 40 位十六进制的分支名,这挡住了分支名变体。但:① 你信任的 marketplace 里,某个插件的仓库可能托管在别处(marketplace 和插件仓库不必同源);② Gemini CLI 的 FETCH_HEAD 变体和分支名是否像 SHA 完全无关;③ 这是「减少一条路径」,不是「关闭这个漏洞」。升级 Agent 才是完整缓解。
GitHub does reject 40-hex branch names, which blocks the branch-name variant. But: ① inside a marketplace you trust, an individual plugin's repository may be hosted elsewhere (the marketplace and the plugin repo need not share a host); ② Gemini CLI's FETCH_HEAD variant has nothing to do with whether a branch name looks like a SHA; ③ this narrows one path, it does not close the vulnerability. Updating the agent is the complete mitigation.
不能,而且这是整件事最反直觉的地方。攻击的第①②③步里,marketplace 审到的每一版都真的是无害的——审核没有失职。被换掉的内容发生在第④步,发生在 marketplace 的视野之外、在客户端解析的那一瞬间。AIR 说得很直接:钉扎是在客户端解析的,所以 marketplace 无法强制执行它自己宣传的那个保证。
It cannot, and this is the most counter-intuitive part. In steps ①②③, every version the marketplace reviewed really was benign — review did not fail. The swap happens at step ④, outside the marketplace's view, in the instant of client-side resolution. AIR puts it plainly: the pin is resolved on the client, so no marketplace can enforce the guarantee it advertises.
关掉自动更新去掉的是「零点击」这个修饰词,不是漏洞本身。下一次你手动 claude plugin update 或者装一个新插件时,同一句 git checkout 照样跑,照样被骗。自动更新只是决定了什么时候触发,不决定会不会中招。
Disabling auto-update removes the "zero-click" qualifier, not the vulnerability. The next time you manually run claude plugin update or install a new plugin, the same git checkout runs and is fooled the same way. Auto-update decides when it triggers, not whether you are exposed.
凡是你依赖某个「保证」的地方,问一句:这个保证是谁在什么时候、用什么动作兑现的?「我钉扎了 SHA」是一个意图,不是一个保证;真正兑现它的动作是「签出之后把 HEAD 解析出来比一遍」。意图和动作之间的那条缝,就是所有这类漏洞的栖身之处。
Wherever you rely on a "guarantee," ask: who redeems it, when, and with what concrete action? "I pinned the SHA" is an intent, not a guarantee; the action that redeems it is "resolve HEAD after checkout and compare." The gap between intent and action is where this whole class of vulnerability lives.
9 · 术语表9 · Glossary
- SHA pinning(SHA 钉扎)
- 把某个依赖锁定到一个具体的 commit 哈希,而不是「最新版」或某个分支。目的是让「审查过的代码」和「实际跑的代码」永远是同一份。
- ref / object id
- ref 是可变的名字(分支、tag);object id 是那串 40 位十六进制的内容哈希。git 解析一个字符串时,ref 优先于 object id——这是 Plugin4Shell 的地基。
- default branch(默认分支)
- 仓库被
git clone时自动检出的那个分支。攻击必须把恶意分支设为默认——非默认分支只会成为 remote-tracking ref,签出会回落到 commit 对象。 - FETCH_HEAD
- git 在
git fetch后写下的一个文件,记录刚取回的内容。它通常被当作一个「特殊名字」使用,但如果仓库里真有一个叫这个名字的分支,解析结果就会变。 - zero-click(零点击)
- 受害者不需要做任何事(不点链接、不确认弹窗、不安装)即可被攻击。这里的零点击来自插件后台自动更新。
- rug-pull(抽梯子)
- 先建立信任、被广泛采用之后,再把内容换成恶意版本。供应链攻击里最经典的形态。
- coordinated disclosure(协同披露)
- 研究者先私下通知厂商、给出修复窗口(常见 90 天),到期再公开。AIR 走的就是这个流程:5 月发现,6 月通知,9 月 17 日公开。
- SHA pinning
- Locking a dependency to a specific commit hash rather than "latest" or a branch, so that the code reviewed and the code running are always the same code.
- ref / object id
- A ref is a mutable name (branch, tag); an object id is the 40-hex content hash. When git resolves a string, refs take precedence over object ids — the bedrock of Plugin4Shell.
- default branch
- The branch checked out automatically on
git clone. The attack requires the malicious branch to be the default — a non-default branch arrives only as a remote-tracking ref and the checkout falls back to the commit object. - FETCH_HEAD
- A file git writes after
git fetchrecording what was just retrieved. It is normally treated as a special name, but if the repository genuinely has a branch by that name, resolution changes. - zero-click
- The victim needs to do nothing — no link, no prompt, no install — to be compromised. Here it comes from background plugin auto-update.
- rug-pull
- Build trust, get widely adopted, then swap the contents for a malicious version. The classic shape of a supply-chain attack.
- coordinated disclosure
- Researchers notify vendors privately with a fix window (commonly 90 days), then publish. AIR followed exactly this: found in May, disclosed in June, published 17 September.
10 · 来源清单10 · Sources
本文的事实性内容均来自以下来源,调研日期 2026-09-21。安全领域的修复状态变动很快——Copilot 的补丁状态请以微软官方公告为准。
Every factual claim here comes from the sources below, researched on 2026-09-21. Patch status in security moves fast — check Microsoft's own announcements for Copilot's current state.
- 一手研究(核心来源):AIR Security, Plugin4Shell — Zero Click RCE Vulnerability found in top 4 most popular coding agents,作者 Or Nevo / Dor Granat / Niv Hoffman,2026-09-17。技术细节、攻击链、时间线、修复版本号均出自此文。
- 独立报道:Help Net Security, Zero-click RCE vulnerability hit four major AI coding agents, two remain unpatched,2026-09-18。用于交叉验证修复状态与影响范围。
- 独立报道:The Register, AI coding agents' 0-click RCE flaw could hand attackers keys to the kingdom,2026-09-17。
- 相关前序研究:AIR Security, MCPJacking: 155 Hijackable MCPs Discovered Live in the Official MCP Marketplace,2026-08-27。
- git 歧义 ref 背景:Teddy Katz, Stealing a few more GitHub Actions secrets,2022;以及 GitLab issue Ambiguous branch name exploitation。用于说明该 git 行为是已知的、长期存在的。
- 官方文档:Claude Code, Create and distribute a plugin marketplace(
strictKnownMarketplaces/extraKnownMarketplaces的说明来源)。 - 版本上下文:Claude Code changelog(经 Releasebot 聚合),用于核对 v2.1.277 / v2.1.278 的当前版本状态。
- Primary research (core source): AIR Security, Plugin4Shell — Zero Click RCE Vulnerability found in top 4 most popular coding agents, by Or Nevo / Dor Granat / Niv Hoffman, 2026-09-17. All technical detail, the attack chain, the timeline and the fix versions come from this piece.
- Independent reporting: Help Net Security, Zero-click RCE vulnerability hit four major AI coding agents, two remain unpatched, 2026-09-18. Used to cross-check patch status and scope.
- Independent reporting: The Register, AI coding agents' 0-click RCE flaw could hand attackers keys to the kingdom, 2026-09-17.
- Related prior research: AIR Security, MCPJacking: 155 Hijackable MCPs Discovered Live in the Official MCP Marketplace, 2026-08-27.
- Background on ambiguous git refs: Teddy Katz, Stealing a few more GitHub Actions secrets, 2022; and GitLab issue Ambiguous branch name exploitation. Cited to show this git behaviour is known and long-standing.
- Official documentation: Claude Code, Create and distribute a plugin marketplace (source for
strictKnownMarketplaces/extraKnownMarketplaces). - Version context: Claude Code changelog (via Releasebot), used to confirm current version state around v2.1.277 / v2.1.278.
① 没有 CVE 编号——截至调研时未找到公开分配的 CVE,本文因此不给编号;② 没有野外利用证据——AIR 展示的是 PoC 与前序研究里的真实劫持,但没有证据表明 Plugin4Shell 本身已在野外被利用,不要把它读成「你已经被黑了」;③ 第 7 节的包管理器对比是概念性的,细节以各生态官方文档为准。
① No CVE identifier — none was found publicly assigned at research time, so none is quoted here; ② no evidence of exploitation in the wild — AIR demonstrates PoCs and real hijacks from its prior research, but nothing indicates Plugin4Shell itself has been exploited in the wild; do not read it as "you have already been breached"; ③ the package-manager comparison in section 7 is conceptual — consult each ecosystem's documentation for specifics.
主页