Mingyu's Library主页
深度学习 · 2026-09-21 Deep Dive · 2026-09-21

Agent 插件供应链安全Plugin4Shell,与那个没兑现的 SHA 钉扎 Agent Plugin Supply Chain SecurityPlugin4Shell, and the SHA Pin That Never Was

四家做编码 Agent 的公司,各自独立实现了插件安装,然后犯了同一个错:按 commit SHA 签出,却从不确认签出后真的落在那个 commit 上。这一份文档把这个错误拆到 git 的解析规则层面,再带你把自己的机器检查一遍。

Four companies built plugin installation for their coding agents independently, and all four made the same mistake: check out a pinned commit SHA, then never verify the working tree actually landed on it. This document takes that mistake apart down to git's own ref-resolution rules, then walks you through auditing your own machine.

调研时间:2026-09-21 · 主要来源:AIR Security 原始研究(2026-09-17)、Help Net Security、The Register、Claude Code changelog、Claude Code 官方文档 · 全文事实均标注来源,推断与观点单独标记
Researched 2026-09-21 · Primary sources: AIR Security original research (2026-09-17), Help Net Security, The Register, Claude Code changelog, Claude Code docs · Every factual claim is sourced; inference and opinion are marked separately

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 上都能拿到零点击的远程代码执行。

In one sentence

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.

你以为发生的事 marketplace 钉扎 SHA aaa… git checkout aaa… 解析为 commit 对象 ✓ 被审查过的代码 和审查那一刻一致 实际可能发生的事 marketplace 钉扎 SHA aaa…(没变) git checkout aaa… 解析为同名「分支」 ✗ 攻击者控制的代码 Agent 仍报告「安装成功」 两条路径里,marketplace 和钉扎值完全相同 —— 差别只在客户端怎么解析那串字符 What you think happens marketplace pins SHA aaa… git checkout aaa… resolves to the commit ✓ the reviewed code identical to review time What can actually happen marketplace pins SHA aaa… (unchanged) git checkout aaa… resolves to a branch ✗ attacker-controlled code agent still reports success The marketplace and the pin value are identical on both paths — only client-side resolution differs
图 1:同一个钉扎值,两种解析结果。信任链断在客户端,而不是 marketplace。(依据 AIR 研究绘制)
Figure 1: One pin value, two resolutions. The trust chain breaks on the client, not at the marketplace. (Drawn from AIR's research)
修复状态并不整齐(截至 2026-09-21) Patch status is uneven (as of 2026-09-21)

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."

The Story of Skills 自建恶意 skill 走红 26,000+ agent SkillJacking 925 个在用 skill 被劫持 134,000 agent MCPJacking · 08-27 官方 marketplace 中 155 个可劫持 MCP Plugin4Shell 防上面三者的机制 本身失效 · 09-17 共同结论:Agent 的信任边界不在模型,在 add-on 的分发链路上 The Story of Skills a planted skill went viral 26,000+ agents SkillJacking 925 in-use skills hijacked 134,000 agents MCPJacking · Aug 27 in the official marketplace 155 hijackable MCPs Plugin4Shell the defence against all three itself fails · Sep 17 Shared lesson: an agent's trust boundary is the add-on distribution path, not the model
图 2:AIR 的四项研究构成一条完整链条。数字来自 AIR 官方披露。
Figure 2: AIR's four pieces of research form one chain. Figures are from AIR's own disclosures.

行业对前三幕给出的答案就是 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

术语:ref 与 object id

object id 就是那串 40 位十六进制的 commit SHA,它直接指向仓库里的一个对象。ref 是「人类起的名字」——分支名、tag 名都是 ref,它是一个指向某个 object id 的可变指针。你敲 git checkout main 用的是 ref,敲 git checkout a1b2c3… 用的是 object id。

Terms: ref vs 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.

两个必要条件,缺一不可 Two necessary conditions

① 托管方允许 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.

这不是四次独立的实现失误 This is not four independent slips

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:

  1. 种下攻击者发布一个真的无害的插件到可信 marketplace,钉扎在 commit aaa…,顺利通过审查。
  2. 被采用人们开始安装。每次安装都钉扎到 aaa…——那个被审查过、可信的版本。
  3. 版本更新攻击者发布一次常规更新,marketplace 重新钉扎到新 commit bbb…。这个新版本仍然是无害的,审查照样通过。
  4. 抽梯子攻击者建一个名叫 bbb… 的分支,设为默认分支,指向恶意代码。被钉扎的那个 commit 本身可以原封不动。
  5. 自动更新即 RCE钉扎值变了,触发每个 Agent 的后台自动更新。签出把 bbb… 解析成分支,恶意代码落地运行。无提示,无点击。
  1. PlantThe attacker publishes a genuinely benign plugin to a trusted marketplace, pinned at commit aaa…. It passes review.
  2. AdoptionPeople install it. Every install is pinned to aaa… — the reviewed, trusted version.
  3. 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.
  4. 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.
  5. 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.
① 种下 无害插件 pin=aaa… ② 被采用 用户安装 审查通过 ③ 版本更新 仍然无害 pin=bbb… ④ 抽梯子 建分支 bbb… 设为默认 ⑤ RCE 后台自动更新 零交互 注意:①②③ 每一步在 marketplace 侧看都完全正常 —— 审查、钉扎、更新流程全部照章执行 被攻破的只有第 ④ 步,而它发生在 marketplace 看不见的地方 ① Plant benign plugin pin=aaa… ② Adoption users install review passed ③ Version bump still benign pin=bbb… ④ Rug-pull branch "bbb…" set as default ⑤ RCE auto-update no interaction Note: steps ①②③ look entirely normal from the marketplace side — review, pin and update all proceed by the book Only step ④ is the compromise, and it happens where the marketplace cannot see it
图 3:五步攻击链。AIR 同时给出了第二条路径——直接接管一个别人写的、marketplace 已经信任的插件仓库,跳过①②③,从第④步开始。
Figure 3: The five-step chain. AIR also describes a second path — hijack the repository behind a plugin someone else wrote that the marketplace already trusts, skipping ①②③ and starting at step ④.
最刺人的一句话 The line that stings

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 requested FETCH_HEAD is 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
AgentStatusVersion / date
Claude CodePatched2.1.179 (AIR records confirmation on 2026-06-17)
OpenAI CodexPatched0.146.0 (verified 2026-08-12)
GitHub CopilotUnpatchedMicrosoft received the disclosure; no fix as of this research
Gemini CLIWill not be fixedOn 2026-08-04 Google confirmed deprecation; migrate to Antigravity
时间线(AIR 官方披露) Timeline (AIR's own disclosure)

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.

一个务实的收口顺序 A pragmatic lockdown order

① 先升级(唯一的完整缓解);② 盘点并卸掉闲置插件(缩小受害面);③ 企业侧用 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.

不用那两款工具也值得知道 Worth knowing even if you don't use those two

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 / yarnlockfile 里记录 tarball 的 integrity 哈希,安装时对下载内容重新计算并比对客户端,且真的比对了
Go modules模块内容哈希写进 go.sum,并有公共 checksum database 做交叉见证客户端 + 第三方公证
容器镜像按 digest(内容哈希)拉取,运行时校验镜像层哈希客户端,且真的比对了
Agent 插件(修复前)记录 commit SHA,执行 git checkout没有比对这一步
EcosystemHow integrity is guaranteedWhere verification happens
npm / yarnTarball integrity hashes recorded in the lockfile; the downloaded content is re-hashed and compared on installClient-side, and actually compared
Go modulesModule content hashes in go.sum, cross-witnessed by a public checksum databaseClient-side plus third-party notarization
Container imagesPulled by digest (content hash), with layer hashes verified at runtimeClient-side, and actually compared
Agent plugins (pre-fix)Record a commit SHA, run git checkoutThe 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 上的 marketplace,所以没事」 Misreading 1: "I only use GitHub-hosted marketplaces, so I'm fine"

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 加强审核就能解决」 Misreading 2: "Tighter marketplace review would fix this"

不能,而且这是整件事最反直觉的地方。攻击的第①②③步里,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.

误解三:「关掉自动更新就行了」 Misreading 3: "I'll just turn off auto-update"

关掉自动更新去掉的是「零点击」这个修饰词,不是漏洞本身。下一次你手动 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.

可以带走的一条通则 One general rule to take away

凡是你依赖某个「保证」的地方,问一句:这个保证是谁在什么时候、用什么动作兑现的?「我钉扎了 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 fetch recording 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.

  1. 一手研究(核心来源):AIR Security, Plugin4Shell — Zero Click RCE Vulnerability found in top 4 most popular coding agents,作者 Or Nevo / Dor Granat / Niv Hoffman,2026-09-17。技术细节、攻击链、时间线、修复版本号均出自此文。
  2. 独立报道:Help Net Security, Zero-click RCE vulnerability hit four major AI coding agents, two remain unpatched,2026-09-18。用于交叉验证修复状态与影响范围。
  3. 独立报道:The Register, AI coding agents' 0-click RCE flaw could hand attackers keys to the kingdom,2026-09-17。
  4. 相关前序研究:AIR Security, MCPJacking: 155 Hijackable MCPs Discovered Live in the Official MCP Marketplace,2026-08-27。
  5. git 歧义 ref 背景:Teddy Katz, Stealing a few more GitHub Actions secrets,2022;以及 GitLab issue Ambiguous branch name exploitation。用于说明该 git 行为是已知的、长期存在的。
  6. 官方文档:Claude Code, Create and distribute a plugin marketplace(strictKnownMarketplaces / extraKnownMarketplaces 的说明来源)。
  7. 版本上下文:Claude Code changelog(经 Releasebot 聚合),用于核对 v2.1.277 / v2.1.278 的当前版本状态。
  1. 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.
  2. 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.
  3. Independent reporting: The Register, AI coding agents' 0-click RCE flaw could hand attackers keys to the kingdom, 2026-09-17.
  4. Related prior research: AIR Security, MCPJacking: 155 Hijackable MCPs Discovered Live in the Official MCP Marketplace, 2026-08-27.
  5. 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.
  6. Official documentation: Claude Code, Create and distribute a plugin marketplace (source for strictKnownMarketplaces / extraKnownMarketplaces).
  7. Version context: Claude Code changelog (via Releasebot), used to confirm current version state around v2.1.277 / v2.1.278.
诚实说明:本文没能覆盖的部分 Honest note: what this document could not cover

没有 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.