Plugin4Shell:四个编码 Agent 的零点击 RCE,以及那个从不校验的「针脚」
2026 年 9 月 18 日,安全公司 Air Security 披露了一类被称为 Plugin4Shell 的漏洞:它击穿的是 SHA pinning——也就是「把插件锁死在一个已审查过的 commit」这套机制。受影响的是四个最主流的编码 Agent:Anthropic 的 Claude Code、OpenAI 的 Codex、微软的 GitHub Copilot,以及 Google 的 Gemini CLI。Air 称其为「AI Agent 生态的第一个供应链漏洞」,并用一句话概括了最难受的地方:受害者不需要装插件装得很随意,他们只要装了一个来自可信市场、经过审查、被正确 pin 住的插件就够了。
一个 pin 只有在被校验之后才真正有意义
一、漏洞本体:检出了针脚,却从不校验它
四个 Agent 的缺陷属于同一类:它们都会取出被 pin 住的 commit hash,然后让 Git 去检出它——但没有任何一个在事后校验「工作树是不是真的落在了那个 hash 上」。Air 在 2026 年 5 月发现该问题并做出对四个 Agent 均可用的 PoC,6 月私下通知全部四家厂商。三个月后公开披露时,仍未分配任何 CVE 编号。修复进度则分成了三种:Anthropic 在 Claude Code 2.1.179 中修复;OpenAI 在 Codex 0.146.0 中修复;微软在披露时尚未给出 Copilot 的修复;Google 更彻底——直接把 Gemini CLI 弃用,转向新产品 Antigravity,而不是给它打补丁,这意味着所有现存安装会无限期暴露。
# The defect, simplified. The agent fetches the pin, checks it out,
# and never confirms what actually landed on disk.
git clone https://host/plugin.git && cd plugin
git checkout "$PINNED_SHA" # Git's ref resolution decides the meaning
# ...and installs whatever is now in the working tree, trusting the pin.
# There is no post-checkout assertion. No mismatch error. No signal.
# Four independent implementations shared this same gap.二、两种变体:一个借分支名,一个借 FETCH_HEAD
第一种变体影响 Claude Code、Codex 和 GitHub Copilot:Agent 克隆插件仓库后执行 git checkout 加那个 40 位 hash。如果攻击者控制了该仓库,就可以创建一个「名字恰好等于这个 hash」的分支,并把它设为仓库默认分支。当分支名与原始 commit 对象在引用解析中产生歧义时,Git 可能优先命中分支,于是检出静默地落在了攻击者控制的分支内容上,而不是 pin 想强制的那个不可变 commit。第二种变体只影响 Gemini CLI:它执行的是 git fetch origin 然后 git checkout FETCH_HEAD;如果攻击者把默认分支命名为 FETCH_HEAD,同样的歧义会反向出现——检出解析到的是那个分支,而不是刚刚 fetch 下来的合法 commit,于是被 pin 的代码被整个丢弃。两种变体都需要攻击者控制插件源码仓库,而这可以通过「先发布一个人畜无害的插件、之后再变坏」或「直接接管一个已被信任的仓库」两种方式实现。
# The attack. The attacker controls the plugin repository.
# Create a branch whose NAME is the pinned 40-character hash,
# and make it the repository's default branch.
SHA=$(git rev-parse HEAD) # the hash the marketplace will pin
git checkout -b "$SHA" # a branch that looks like a commit
git commit -am "routine maintenance" # now it carries malicious code
git push origin "$SHA"
# On hosts that allow a 40-hex branch name (Bitbucket, self-hosted Git),
# ref resolution can favour the branch over the raw commit object when
# the two are ambiguous. `git checkout $SHA` then lands on attacker
# code while the reported pin still looks perfectly intact.三、为什么是「零点击」:它命中那些做对了每一步的人
整个利用链是安全研究者近年开始称为 rug-pull 的模式:攻击者发布一个真正良性的插件,通过市场审查并被 pin 到初始 commit;等它积累起装机量后,攻击者发一个看起来平平无奇的更新,市场顺势把安装重新 pin 到新的 commit hash;随后攻击者创建那个以新 hash 命名的分支(对 Gemini CLI 则是 FETCH_HEAD),指向恶意代码;下一次已安装 Agent 的后台自动更新流程重新检查插件时,替换后的代码就执行了——零用户交互,也没有任何错误提示。关键在最后一句:Claude Code 与 Codex 的插件自动更新默认开启,所以这条链完全不需要用户动手。这也意味着它绕过了「我只装审查过的插件」这类自律:受害者的行为完全符合安全模型的期望。Air 的先期研究让这条链的两端都得到验证——它自建的插件曾扩散到超过 26,000 个 Agent 后才被下架;而在其称为 SkillJacking 的研究中,已有 925 个被实际使用中的技能被从原维护者手中劫持,触达 134,000 个 Agent。
# The durable fix: assert the RESOLVED state after checkout,
# not the reference you requested. Abort the moment they disagree.
set -euo pipefail
git clone --no-checkout "$URL" plugin && cd plugin
git fetch --depth 1 origin "$PINNED_SHA"
git checkout --detach "$PINNED_SHA"
RESOLVED=$(git rev-parse HEAD)
if [ "$RESOLVED" != "$PINNED_SHA" ]; then
echo "pin bypass detected: asked for $PINNED_SHA, got $RESOLVED" >&2
exit 1
fi
# A pin is only meaningful if the tree is also clean.
[ -z "$(git status --porcelain)" ] || { echo "dirty working tree" >&2; exit 1; }
echo "verified: $RESOLVED"四、为什么市场自己补不上这个洞
一个很自然的反应是「让市场去修」。但那道检查运行在 Agent 内部,而不是市场侧,所以任何市场都无法单方面提供保证。一个可行的部分缓解是限制插件托管平台:GitHub 会直接拒绝一个 40 位十六进制字符的分支名,但 Bitbucket 以及任何自建 Git 服务器是允许的——而受影响的 Agent 官方都支持这些托管方式。把插件来源限制在「拒绝 SHA 形状分支名」的平台上能削掉第一种变体的大部分风险,但它既缩小不了 Gemini CLI 的那个变体,也把自建 Git 的用户挡在了门外。Air 的结论是:修复必须由 Agent 自己在检出之后立即完成,方式是断言「解析后的 HEAD」——而不是最初请求的那个 ref——与 pin 的 SHA 完全一致,不一致就中止安装。
// Inventory what each installed plugin ACTUALLY resolves to, weekly,
// independently of what the agent self-reports.
const dirs = ["~/.claude/plugins/*", "~/.codex/plugins/*", "~/.copilot/plugins/*"];
for (const glob of dirs) {
for (const d of await expand(glob)) {
if (!(await isRepo(d))) continue;
const head = await git(d, "rev-parse HEAD");
const dirty = await git(d, "status --porcelain");
console.log(d, "resolved=" + head, "dirty_files=" + dirty.split("\n").filter(Boolean).length);
}
}
// Diff this list against the marketplace pin list. Any mismatch is an
// incident, not a curiosity: it means reviewed code is not what runs.五、你现在能做的五件事
第一,盘点。列出发开岗位与 CI 环境里每一个编码 Agent 及其版本,确认落在补丁线之上:Claude Code 2.1.179 或更高、Codex 0.146.0 或更高;Copilot 与 Gemini CLI 目前没有可对照的厂商修复。第二,对于无法确认已修复的 Agent,关掉插件自动更新——这会剥掉攻击链里「零点击」的那一节,让任何替换至少需要一次手动重装,而手动动作是人和端点控制能看住的。第三,把每个已安装插件当成正式的供应链依赖来管:记录它当前实际解析到哪个 commit,并独立于 Agent 自报的 pin 状态去复核这个解析结果。第四,给插件来源加一道平台门禁,拒绝接受 SHA 形状分支名的托管。第五,把最省事但最有效的那层做上——用最小权限沙箱把插件跑起来,让一次成功的 pin 绕过只能拿到狭窄、可监控的影响面,而不是开发者账号的完整权限。
# Two cheap policy gates that close the widest path first.
# 1) Only install plugins from hosts that reject SHA-shaped branch names.
if git ls-remote --heads "$URL" | awk '{print $2}' | grep -qE 'refs/heads/[0-9a-f]{40}$'; then
echo "host allows SHA-named branches - refusing this plugin source" >&2
exit 1
fi
# 2) Where you cannot patch yet, turn OFF background auto-update.
# Patched floors: Claude Code 2.1.179+, Codex 0.146.0+.
# GitHub Copilot had no vendor fix at disclosure and Gemini CLI was
# deprecated rather than patched, so existing installs stay exposed.六、真正的教训:校验「解析后的状态」,而不是「你请求的标识」
把 Plugin4Shell 放到更大的图景里看,它揭示的是一个反复出现、而且一层层往上走的模式。CSA 研究笔记把它和另外两个案例并列:GuardFall 发现十一个流行开源编码 Agent 中有十个能在命令安全护栏存在的情况下被诱骗执行任意 shell 命令,原因是护栏检查命令的时机早于 shell 自己的展开与解析逻辑;Plugin4Shell 则是同一个结构性错误往上挪了一层——pin 这道控制检查并记录了「请求的引用」,却从不检查请求与实际是否一致。结论对任何做 Agent、市场或包管理器的人都一样:一个哈希 pin 要真正成为完整性控制,它必须断言解析之后的状态,而不是仅仅记录你请求了哪个标识。当「只装审查过的代码」这层承诺可以被静默绕过时,剩下的就只是最小权限沙箱和独立复核——而这两样,恰好是过去十年供应链安全教给人类的那一课,现在轮到 Agent 来补了。
分支名可以长得像 commit hash——这就是一切的入口
沙箱决定了一次绕过能走多远
📌 常见问题 FAQ
Plugin4Shell 是什么?
它是 Air Security 于 2026 年 9 月 18 日披露的一类零点击远程代码执行漏洞,绕过的是 SHA pinning 这道完整性控制:四个主流编码 Agent 在检出被 pin 的 commit 之后,都不校验工作树是否真的落在那个 commit 上,攻击者因此可以替换已审查的插件代码,而 pin 看起来依旧完好。
哪些 Agent 受影响?修好了吗?
Claude Code、OpenAI Codex、GitHub Copilot 与 Google Gemini CLI 四个都受影响。Anthropic 在 Claude Code 2.1.179 修复、OpenAI 在 Codex 0.146.0 修复;披露时微软尚未给出 Copilot 的修复,Google 则直接弃用了 Gemini CLI(转向 Antigravity),未打补丁,现存安装会持续暴露。
为什么说它是「零点击」?
因为插件自动更新在 Claude Code 和 Codex 中默认开启。当市场把 pin 更新到新的 commit hash 时,已安装插件会在后台自动重新执行同一段 git checkout,替换后的代码随之执行,用户无需任何操作,也不会看到错误提示。
我该先做什么?
先盘点并升级到已修复版本(Claude Code 2.1.179+、Codex 0.146.0+);对无法确认修复的 Agent 关闭插件自动更新;把已安装插件当作供应链依赖,记录并独立复核其实际解析到的 commit;限制插件来源平台;并用最小权限沙箱运行插件以限制影响面。
为什么市场侧修不了这个问题?
因为校验逻辑运行在 Agent 内部而不是市场侧,任何市场都无法单方面提供保证。限制托管平台能挡住第一种变体的大部分风险,但挡不住 Gemini CLI 的 FETCH_HEAD 变体,也会排除自建 Git 服务器的用户。
📚 参考资料
- Air Security — Plugin4Shell: Zero-Click RCE Vulnerability Found in Top 4 Most Popular Coding Agents (September 18, 2026)
- Cloud Security Alliance — Research Note: Plugin4Shell SHA-Pinning Bypass Enables AI Coding Agent RCE (September 19, 2026)
- Help Net Security — Zero-click RCE vulnerability hit four major AI coding agents, two remain unpatched (September 18, 2026)
- The Hacker News — Plugin4Shell Lets Repository Owners Swap Pinned Plugin Code Across Four AI Coding Agents
- The Register — AI coding agents' 0-click RCE flaw could hand attackers keys to the kingdom (September 17, 2026)