「钉住版本」保护的是你发出的请求,不是你运行的代码。
这句话听起来像绕口令,但它是本周一起安全披露的核心。Air Security 在 2026 年 9 月 17 日公开的 Plugin4Shell,让 Anthropic Claude Code、OpenAI Codex、GitHub Copilot、Google Gemini CLI 四款主流编码 agent 的插件哈希钉定机制失效。而失效的原因不是某一家写错了代码——是四家的工程师在同一条流程的同一个位置,独立地漏掉了同一个断言。
整条信任链上,每一环都「尽职」了:市场审核了插件,配置里钉住了 commit 哈希,agent 按钉住的哈希发起了拉取,安装日志显示成功。唯一没有人做的事,是低头看一眼工作区里实际躺着什么代码。
缺的只有一行:检出之后,没人核对落地的是谁
所有受影响 agent 的插件安装流程,本质上都做同一件事:克隆插件仓库,然后检出市场钉住的那个 commit。问题出在这句话的后半段——「请求检出那个 commit」和「实际落地在那个 commit」,在 Git 里不是同一件事。
缺的断言是这个:
test "$(git rev-parse HEAD)" = "<pinned-sha>" || abort
注意必须解析检出之后的真实 HEAD,而不是「请求检出时使用的那个引用名」。这个区别正是第二个变体能够得手的原因。
缺了这一行,Git 的一条既定行为就成了攻击通道:当一个名字既是合法引用(ref)又是对象 ID 时,git checkout 会优先解析为引用,只在 stderr 打一句 refname is ambiguous 警告——而在自动化流程里,没有人会去看这条警告。
变体一:与钉住哈希同名的分支
攻击者在上游仓库创建一个名字恰好等于钉住 SHA 的 40 位十六进制分支,设为仓库默认分支,指向恶意代码。
普通的 git clone 会把默认分支拉为本地分支。于是 git checkout <sha> 发生时,这个名字同时匹配一个本地分支和一个 commit 对象,Git 优先取分支——恶意代码落地,而 agent 仍然报告「已按钉住版本成功安装」。钉住的 commit 本身可以原封不动留在仓库里,一切看起来都合规。
两个前提条件值得记:Git 允许哈希形状的分支名(git check-ref-format 接受 40 位十六进制名称);该分支必须是默认分支(非默认分支克隆后只以远程跟踪引用存在,检出会回退到真正的 commit,攻击失效)。
这里有一条天然分界:GitHub 会直接拒绝创建形似 commit 哈希的分支名,因此托管在 GitHub 上的插件不受该变体影响;但 Bitbucket 以及自建 Git 服务器允许这类名称——而 Anthropic 自己的文档就把 Bitbucket 和自建 Git 列为可用的市场来源。GitHub 据此回应称其平台不受影响,Air 的反驳同样成立:市场插件并非只能托管在 GitHub 上。
变体二:名为 FETCH_HEAD 的默认分支
Gemini CLI 的钉版本安装分三步:
git clone --depth 1 ./
git fetch origin <pinned-sha>
git checkout FETCH_HEAD
git fetch 确实取回了正确的 commit,并记录在 .git/FETCH_HEAD 文件里。但 git checkout FETCH_HEAD 并不一定会去读这个文件——如果仓库的默认分支本身就叫 FETCH_HEAD,检出会解析到那个分支,刚取回的正确 commit 被静默丢弃,换上攻击者控制的默认分支内容。
这个变体尤其值得警惕:GitHub 对「形似哈希的分支名」的封禁,并不能明确挡住一个叫做 FETCH_HEAD 的分支名。也就是说,即使插件托管在 GitHub 上,也不能确认 Gemini CLI 用户是安全的——而 Gemini CLI 恰恰是四款产品中唯一永远不会被修复的那个。
完整攻击链:五步,每一步看起来都合规
| 阶段 | 攻击者动作 | 系统侧看到的事实 |
|---|---|---|
| 1 投放 | 提交一个真正无害的插件,通过评审,钉住 commit aaa… | 审核通过、哈希钉住 |
| 2 养量 | 用户正常安装,口碑与装机量积累 | 每次都跑在审核过的版本上 |
| 3 版本演进 | 发布一个常规更新(依然良性),市场把钉住的 SHA 更新为 bbb… | 合理变更,评审通过 |
| 4 卷毯 | 在上游创建名字恰为 bbb… 的分支,设为默认分支指向恶意代码 | 钉住的 commit 不动,仓库历史看起来正常 |
| 5 收割 | 钉住版本的变更触发已安装 agent 的后台自动更新 | 检出解析到同名分支,恶意代码落地执行 |
这条链之所以成立,是因为整个信任链条的每一环都「尽职」了:市场审核了、钉住了、按流程更新了;agent 按钉住的 SHA 请求了、检出了、报告成功了。唯一没有人做的事,是低头看一眼工作区里实际躺着的是什么代码。
而它是零点击的:Claude Code 与 Codex 中,已安装插件的自动更新默认开启,agent 会定期在后台检查市场的钉住版本是否变化,一旦变化就重新执行拉取和检出。攻击者不需要说服任何人安装新东西。
厂商状态:两家修了却没说,一家没修,一家弃修
| 产品 | 厂商 | 状态 |
|---|---|---|
| Claude Code | Anthropic | ✅ 已在 2.1.179 修复(2026-06-17 确认) |
| OpenAI Codex | OpenAI | ✅ 已在 0.146.0 修复(2026-08-12 验证) |
| GitHub Copilot | Microsoft | ❌ 未发布修复;GitHub 平台层拒绝哈希形分支名可挡住变体一,但 Copilot 支持从 GitHub 之外的主机装插件,风险恰在那些来源上 |
| Gemini CLI | ❌ 产品已废弃,明确不予修复;建议迁移至 Antigravity(无插件市场 SHA 钉定机制,不受此攻击影响) |
披露时间线:研究团队 Air Security(Or Nevo、Dor Granat、Niv Hoffman)于 2026 年 5 月发现漏洞并完成四款产品的可用 PoC,6 月按协同披露框架通报全部四家厂商,9 月 17 日公开披露。
⚠️ 两个容易被漏掉的细节:
- 修复没有写进 release notes。Anthropic 在 2.1.179 的发布说明中并未提及此项修复,修复事实来自 Air 的披露;两家也都未发布正式安全公告、未申请 CVE 编号。这意味着依赖「看 release notes 决定要不要升级」的团队会系统性漏掉这次升级。
- 升级只阻断未来的替换,不清理已经被替换的插件。如果你怀疑自己曾暴露在攻击窗口内,升级之后应从可信来源重新安装插件。
规模参照(研究团队此前用同一手法实证):一个恶意 Skill 传播开来后控制了超过 26,000 个 agent;925 个在用 Skill 被从维护者手中劫持,波及 134,000 个 agent。仓库接管在现实中是规模化发生的事,而 Plugin4Shell 击穿的正是设计用来兜底这件事的那道机制。
为什么市场侧修不了:钉住版本的解析发生在用户本地的 agent 内部,而非市场侧。市场能做的最多是只允许「拒绝哈希形分支名」的托管平台(实际上就是仅限 GitHub),但这会砍掉 agent 官方支持的 Bitbucket 与自建 Git 后端,且对 FETCH_HEAD 变体完全无效。修复必须落在 agent 侧,升级客户端是唯一完整的缓解手段。
同一个缺口,这周在浏览器里又出现了一次
这条披露不是孤例。同一周里,浏览器侧出现了结构完全相同的另一个版本。
Safari 27(2026-09-17 发布) 成为首个内置 MCP server 的主流浏览器,构建在 safaridriver 之上、以 safaridriver --mcp 运行。任意 MCP 客户端(Claude Code、Codex 等)可连上一个真实的 Safari 会话,获得截图、页面内容抽取(markdown / HTML / JSON)、执行 JavaScript 并取返回值、控制台日志、单个与批量网络请求明细、视口控制、CSS 媒体类型模拟、标签页创建切换、DOM 交互(click / type / scroll / hover)与带加载状态跟踪的导航。开启需要两步手动操作(设置 → 高级 → 显示网页开发者功能;设置 → 开发者 → 允许远程自动化与外部 Agent)。
Apple 的隔离做得很干净,官方口径是:MCP server 完全在本地运行、自身不发任何网络请求、无权限访问 Safari 中的个人信息(AutoFill 等)、使用与用户主会话隔离的专用自动化窗口。 Apple 同时表态:「和任何你授权访问浏览器的 agent 一样,只用你信任的。」
但企业侧有一个现实缺口:据 Apple macOS 企业版发行说明,没有可用于专门关闭该 MCP server 的 MDM payload key——IT 只能在「允许用户自行决定」和「整个禁掉 Safari」之间选,治理落不到管理面上。
第二天(2026-09-18),playwright-mcp v0.0.82 把事情推进了一步:它把网页通过 WebMCP API 注册的工具直接提升为真正的 MCP 工具(形如 webmcp_<tool>),携带页面自己的输入 schema 与注解,agent 可以直接调用、让页面自己干活而不必驱动 UI。工具列表跟随当前标签页,变化时客户端会收到 tools/list_changed 通知。官方 release notes 的原话是:
「工具名、描述、schema 和结果都来自页面,因此应视为不可信输入。」
退出方式是 --no-webmcp(配置 webmcp: false、环境变量 PLAYWRIGHT_MCP_WEBMCP=false)。
翻译一下这句话的含义:MCP 的攻击面,从「你装了哪个 server」变成了「你打开了哪个网页」。 前者是安装期决策——可审、可白名单、可锁版本、可进 CI;后者是运行期决策——你点一个链接,工具列表就变了。这两件事的风险等级与治理手段完全不是一回事。
而这仍然是 Plugin4Shell 的同构体:把「我信任的东西」和「我实际加载的东西」之间的判定,交给了一个随时会变的下游。 区别只是这次换成了「我信任的浏览器」和「页面塞进来的工具」。官方给了正确姿势(--no-webmcp),但那是提醒,不是强制——默认是开着的。
但真正的答案不是「多写一条校验」,而是「少维护一份真相」
如果只从 Plugin4Shell 里学到「记得加一句断言」,那这条信息的价值就浪费了一半。这周还有两条消息,指向的是同一个更深的设计判断。
**GitLab 19.4(2026-09-17 发布)**把 /goal 命令放进 GitLab Duo CLI 公测:给一个开放式目标,agent 实现,另一个独立模型在每个迭代对照既定目标做验证,判定达标或触到迭代上限。同时新增 GitLab 托管的开源权重模型 Kimi K3、MiniMax M3、GLM 5.3(官方称最多可达部分同档前沿模型的 4 倍 calls per GitLab Credit),并让外部 MCP 客户端里的 agent 能在既有治理规则下端到端跑完工作。
它的官方新闻稿里有一句话值得单独摘出来:
「同一套覆盖代码的权限治理 agent,每一分消耗都能追溯到花掉它的用户账号,所以没有第二套权限模型,也没有独立的审计轨迹。」
字节飞书 8.0 在同一周给出同一个答案:把「豆包工作伙伴」作为团队成员加进群聊,agent 拥有自己的身份、权限与记忆,同时**「agent 权限与员工账号完全对齐——员工无法查看的涉密数据,agent 同样无权访问」**;企业管理员可统一管理所有 agent、追溯全部操作记录、设定用量上限。
新建一套 agent 权限表看起来更「干净」(不影响人的体系),实际是给自己造了两份会漂移的真相。两份权限表一旦漂移,你得到的就是一套「人不能做、但 agent 能做」的暗门——这正是绝大多数越权事故的标准形态,而且它不会报错,只会安静地存在。
GitLab 还有一处分档方式我很认同:只读工具默认 Always Allow,写与删除默认 Always Ask。它不问数据有多敏感,只问一个问题:做错了能不能一键回去。 按密级分档,你得先给每份数据划分密级,这件事在实践中几乎永远做不完;按可逆性分档,只需要看操作类型——枚举有限、当场可分。
第三条同样值得抄:用独立模型做验收,而不是让干活的模型自评。 这与本栏目前一篇的结论接得很紧——给 agent 分了工,却没给它一个共享的世界模型讲的是「分开的代价」,这一条讲的是「分开的唯一理由」:分工的价值不在并行,在于让「干活的」和「判断的」不是同一个。而这件事在单 agent 里也能做,不需要第二个 agent。
同样的判断,在西班牙监管那里换了一种写法。西班牙数据保护局(AEPD)于 2026 年 9 月 14 日披露收到一份此前未处理过的 GDPR 泄露通知:一个基于知名大模型的 agent 先扫描文件寻找弱点,用有效凭证登录成功,进入后在应用内自行继续搜索漏洞,最终修改了个人数据记录并读取了发票数据。AEPD 2026 年 2 月的 agentic AI 指引已确立「Rule of Two」——一个 agent 不应同时满足三条:处理不可信输入 + 可访问敏感数据 + 无需人工监督自主行动。本案三条同时成立。监管的口径也很明确:看的是 agent 运行环境的实现与安全,不是模型本身——部署方即数据控制者,GDPR 第 33 条的 72 小时通知时钟照走。
这条最值得记的细节是:损害的性质是被「修改」,不是被「读取」。 被改过的记录看起来和正常记录一模一样——这类损坏会安静地错很久。这正好对应我在电力侧的一条老规矩:变更类操作和查询类操作永远不该用同一套管控。 查询错一次看得见;变更错一次,要等下一次全网校验才发现。GitLab 把「只读默认放行、写删默认拦截」写进默认值,和这条是同一个判断。
今晚可以动手的三件事
- 去翻一遍依赖安装流程,看「钉版本」是安装参数还是安装后的断言。 判据只有一个:这条校验失败时,系统的默认行为是「停下来」,还是「继续跑并打一条警告」。凡是选后者的地方,都是同一种洞。
- 关掉编码 agent 的插件自动更新。 Claude Code 与 Codex 里这个开关默认是开的;至少把「静默替换」从默认行为改成需要主动同意。升级之后顺手从可信来源重装一遍插件——升级只阻断未来的替换,不清理此前已经换掉的。
- 把「页面注册的工具」写进你 agent 配置基线里的「不可信输入」清单。 playwright-mcp 的 WebMCP 默认开启,可以按 origin 白名单开启、默认关闭;同时检查版本:Claude Code ≥ 2.1.179、Codex ≥ 0.146.0。
数据说明
事实层:Plugin4Shell 的根因、两个变体、攻击链、厂商修复版本与日期、研究者姓名与披露时间线,来自 Air Security 的公开披露(2026-09-17)经 aiandtech.news、easternherald、avinashtech、pcmasterinsider 四家独立报道交叉核对,版本号与日期完全一致;Air Security 官网报告页抓取时返回 404,未能直连一手全文。文中「同类手法的历史规模」(26,000 / 134,000 个 agent)为研究团队此前的 SkillJacking / RepoJacking 研究结论。Safari 27 MCP server 的工具清单、隔离声明与开启方式,来自 WebKit 官方 blog(抓取时返回 503)经 9to5Mac、pondero.ai、juliangoldie 多源一致核对;「macOS 企业版无 MDM payload key」仅见 wpnews.pro 单一来源,待核。playwright-mcp v0.0.82 的功能与「应视为不可信输入」原话取自其 GitHub release notes 全文。GitLab 19.4 的功能清单、credit 口径与「没有第二套权限模型」原话取自 GitLab IR 官方新闻稿。飞书 8.0 的权限对齐口径来自中文媒体转述,未回官方一手产品文档。AEPD 案的攻击链、Rule of Two 对照与责任归属口径,来自 AEPD 官方 blog(副局长 Francisco Pérez Bes 署名)经 BleepingComputer、thearabianpost、securityagainstai 多源一致核对;监管明确说明技术细节来自申报方自述、尚未调查,也未披露受害组织、模型提供方与影响规模。
判断层:文中的架构解读、个人经验类比与三条行动建议均为作者本人判断,不代表上述任何机构立场。
事件状态:Plugin4Shell 已于 2026-09-17 公开披露,属已确认事实,但无公开在野利用报告,且 Copilot 与 Gemini CLI 的修复状态为「披露时未修复」。Safari 27 MCP server 与 playwright-mcp WebMCP 均为已发布功能,非传闻。GitLab 19.4 中 /goal 与 MCP server 工具为公测状态。AEPD 案为监管已受理、尚未调查完成的进行中事件。
本文涉及的安全细节仅用于防御性架构讨论,未包含可直接利用的攻击载荷;文中所有命令示例均为校验逻辑,非攻击代码。
