Skip to content

필사 모드: 一个 Issue,捅穿整条供应链 — CI 里的智能体是怎么垮的,以及防御到底买到了多少时间

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言 — issue 正文变成命令的那一刻

2026 年 6 月 1 日,GMO Flatt Security 的 RyotaK 公开了 Poisoning Claude Code: One GitHub Issue to Break the Supply Chain。要点正如标题所说 — 仅凭一个 GitHub issue,就能接管使用 Claude Code GitHub Actions 的仓库,受影响的对象里还包括 Anthropic 自己的仓库。

上一篇文章讨论了 Web 智能体的跨站提示注入。如果说那篇是基于论文的防御设计,这一篇则是真实发生在已上线产品中的事件,时间线、CVSS 评分、赏金金额、修复提交全部是公开的。而且还有一个重要的不同 — 同一类缺陷在另一款产品中真的被利用了

这篇文章不是攻击教程,不携带 payload。它只关注智能体构建者真正需要的东西 — 信任边界究竟在哪里断裂,厂商加的防御挡住了什么、又挡不住什么,以及是否存在一个可以说“到这就够了”的点。

舞台 — CI 里的智能体手里握着什么

Claude Code GitHub Actions 是把 Claude Code 接入 CI/CD 的一套工作流。它负责代码审查自动化、issue 分类打标签、基于评论的代码生成之类的工作。运行模式有两种。

  • tag mode — 在 issue 或 PR 评论中提到特定关键词(默认是 @Claude)时触发。
  • agent mode — 工作流配置了 prompt 输入时触发,用来跑斜杠命令或固定的任务。

要操作 GitHub,Claude GitHub App 必须先安装到仓库上,这个 App 拥有的权限是这些 — 读写仓库内容(代码)、读写 issue 和 PR、读写讨论区、读写工作流(Actions)。除非另外指定 token,否则默认就用这个 App 的 token。配置变得简单,攻击面也随之变大。

到这里,Simon Willison 所说的致命三件套已经齐全 — 访问私有数据、暴露于不可信内容、拥有对外通信的手段。三者一旦汇聚到同一个进程,一次注入就能变成一次泄露。CI runner 教科书式地把这三样全占了。

第一步 — “是机器人就放行”这一行

正因为意识到这个风险,该 action 默认阻止没有写权限的用户触发工作流。文档里写得很明确 — action 只能由拥有仓库写权限的用户触发。

这个控制逻辑实现在 checkWritePermissions 函数里。函数会检查 actor 的权限是否为 write 或 admin。但在那之前,有这样一个分支。

// src/github/validation/permissions.ts (修复前)
if (actor.endsWith("[bot]")) {
  core.info(`Actor is a GitHub App: ${actor}`);
  return true;
}

只要 actor 的名字以 [bot] 结尾,无论实际权限如何,一律放行。乍看合理 — GitHub App 通常是仓库管理员安装的可信主体。问题在于,这个假设在公开仓库上并不成立。

正如 GitHub 文档所说,GitHub App 即便没有默认权限,也拥有读取公开资源的隐式权限。而这个隐式权限并不止步于读取。在公开仓库上开 issue 或 PR 不需要额外权限 — 就像任何人都能在别人的仓库上开 issue 一样,GitHub App 也能用自己的安装 token 在任意公开仓库上创建 issue 和 PR。它甚至不需要安装在目标仓库上。

那么绕过方式就清楚了。攻击者创建自己的 App,安装到自己的仓库上(不需要任何特殊权限),再用这个安装 token 在目标公开仓库上开一个 issue。因为 actor 是机器人,checkWritePermissions 返回 true,工作流开始处理攻击者控制的内容。

这里刺痛的地方在于,那个会向 GitHub API 确认 actor 类型是否真的是 User 的 checkHumanActor 检查,在 tag mode 里已经存在。只有 agent mode 缺了这一步。不是没有防御,而是两条路径里只有一条有。

这是第一个教训。靠名字的形状来判断信任,不是信任检查。 endsWith("[bot]") 问的不是“这个主体是否可信”,而是“这个字符串长什么样”。而字符串的形状,是攻击者可以自己挑的。

第二步 — 数据变成命令的地方

一旦绕过成功,攻击者控制的内容就会流入智能体的上下文。RyotaK 举的具体例子,是 anthropics/claude-code 仓库里的 issue 分类工作流。这个工作流指示 Claude 通过 MCP 工具读取 issue,其允许工具列表里有这些内容。

--allowedTools "Bash(gh label list),
                mcp__github__get_issue,
                mcp__github__get_issue_comments,
                mcp__github__update_issue,
                mcp__github__search_issues,
                mcp__github__list_issues"

mcp__github__get_issue 负责读,mcp__github__update_issue 负责写。读工具和写工具并排出现在同一份允许列表里。这个组合,后来正是泄露通道。

注入本身的结构是这样的。攻击者把 issue 正文写得像一条报错信息。Claude 读取这个 issue 时,会把它当作读取失败,并在尝试“恢复”的过程中执行藏在正文里的命令。本该作为数据读入的内容,被当作指令来解释 — 这是带内信令(in-band signaling)的经典失败,和 SQL 注入、XSS 已经证明过的结构一模一样。只不过这里的通道是自然语言。

研究者自己在脚注里诚实地写下的一点很重要 — 光靠那种描述并不能稳定地触发命令执行,真正的利用需要精细得多的提示工程。也就是说,它是概率性的。但 CI 是攻击者可以反复开 issue 的地方。给一个概率性的攻击接上无限次重试,它就变成了确定性的。

第三步 — 看似只读的东西就是泄露通道

一旦能执行命令,接下来就是泄露。这里有两件事叠加在一起。

第一,出于用户体验的考虑,Claude Code 让一部分只读命令可以不经批准就执行。RyotaK 在前一篇文章(2026 年 1 月 12 日)里深挖的正是这一点。echomansedsort 这类命令默认被允许,为防止副作用而对参数设了黑名单,而这份黑名单有 8 个漏洞。这个问题被分配了 CVE-2025-66032,并在 Claude Code v1.0.93 中修复(据 NVD,CVSS 3.1 为 9.8 / CVSS 4.0 为 8.7,CWE-77)。Anthropic 的应对是撤掉黑名单,改用白名单方式。研究者的结论也一样 — 在命令执行这种安全敏感的功能上,黑名单会输。

第二,runner 进程的环境变量里存着密钥。Linux 有一个伪文件,会把当前进程的环境变量原样暴露出来,用一条被允许的读取命令去读它,密钥就进了上下文。然后再用 mcp__github__update_issue 把它写回 issue 正文,攻击者只需要刷新一下自己的 issue 就行。

这里,最终目标为什么是 OIDC 凭证就说得通了。GitHub Actions 支持 OIDC,让工作流能证明自己的身份。只要工作流拥有 id-token 写权限,就能从 GitHub 拿到一个签过名的 token,内容是“我是仓库 Y 里的工作流 X”。Claude Code GitHub Actions 用这套机制来换取一个有特权的 GitHub App 安装 token — 先拿到 OIDC token,提交给 Anthropic 后端的 token 交换端点,后端验证后,返回 Claude GitHub App 的安装 token。

请求这个 OIDC token 所需要的凭证是 ACTIONS_ID_TOKEN_REQUEST_TOKENACTIONS_ID_TOKEN_REQUEST_URL,两者都是环境变量。也就是说,只要这两个泄露,攻击者就能原样复现整个 token 交换流程,拿到一个对仓库内容、issue、PR、工作流都有写权限的安装 token。仓库就此失守。

爆炸半径 — action 仓库自己也在用那套工作流

到这里为止,还只是“一个仓库”的事。真正让它变成供应链问题的,另有原因。

anthropics/claude-code-action — 也就是 action 本身的仓库 — 用的也是同一类 agent mode 工作流。这意味着同样的手法可以在 action 自己的源码里植入恶意代码,而这会传播到所有依赖它的下游仓库。构建这个工具的仓库,自己就暴露在这个工具的漏洞之下。

RyotaK 还额外找到了一个常见的配置错误,即便没有 action 本身的 bug,也能达到相近程度的入侵。把 allowed_non_write_users 设为 *,任何人都能触发工作流,而这正好出现在 Anthropic 自己的仓库和官方示例工作流里。复制了示例的仓库,原样继承了同一个缺陷。

文档把这个选项标记为危险,并要求“只在权限极其有限的工作流中”使用,举的例子是 issue 打标签。但研究者的反驳很犀利。

  • issue 写权限其实并不有限 — 可以删除或编辑已有的 issue。
  • 这个工作流无论如何都需要一个 Anthropic API key。这本身就是敏感密钥,一旦暴露给不可信输入,就已经不合适了。
  • 即便收紧了权限,泄露依然可能发生 — action 默认会把已完成工作的摘要发布到工作流运行的 summary 区块,而这个区块是公开的

把这两点串起来,就是权限提升。先从宽松的分类工作流里(经由 summary 区块)拿到 issue 写 token,再用这个 token,在触发之后编辑一个由可信用户发出的 issue,让严格的 tag mode 工作流把它当作可信上下文来处理。tag mode 拥有 id-token 写权限,OIDC 凭证正是从那里拿到的。从一个外部用户,到完全接管仓库,靠的是把两个工作流拼接起来。

单独看每个工作流,两个都“按预期”运作。 缺陷出在组合上。

这不是假设 — 在 Cline 上真的发生了

到目前为止讲的是负责任的披露,没有造成实际损失(研究者在脚注里说明,他只在自己的测试仓库里做过验证)。但同一类缺陷,在另一款产品上真的被利用了。

2026 年 2 月 9 日,安全研究员 Adnan Khan 公开了 Clinejection。AI 编程工具 Cline 的 issue 分类工作流 — 同样基于 claude-code-action,开放了 Bash、Write、Edit 工具,任何 GitHub 用户开一个 issue 就能触发 — 存在提示注入漏洞。易受攻击的时间窗口是 2025 年 12 月 21 日到 2026 年 2 月 9 日。

这条攻击链很有意思。它没有直接从分类工作流里抠出密钥,而是污染 GitHub Actions 缓存,借此跳到 nightly 发布、npm 发布工作流里,再从那里偷走部署凭证。而这些 nightly 凭证,和生产凭证拥有相同的权限 — VS Code Marketplace 和 OpenVSX 把 token 绑定在发布者身上,而不是扩展本身,npm 的生产版和 nightly 版用的又是同一个包。原以为 nightly 是低等级凭证,但注册表的凭证模型根本不支持这种区分。

披露过程也值得记一笔。Khan 在 2026 年 1 月 1 日通过 GitHub 私密漏洞报告和邮件发出通知,1 月 8 日发了后续邮件,1 月 18 日给 CEO 发了私信,直到 2 月 7 日最后一次尝试,都没有得到回应。2 月 9 日公开披露后,不到一小时就修复了(PR 9211 — 移除 AI 工作流,停用 nightly job 的缓存)。Cline 在 2 月 10 日正式确认并宣布了立即缓解措施,2 月 11 日给出了全面的凭证轮换和审计结果 — 2025 年 12 月 21 日到 2026 年 2 月 9 日之间没有未经授权的发布,npm 上的 41 个版本全部与源码一致。

然而那次审计的窗口到 2 月 9 日就结束了。2026 年 2 月 17 日,有人用被攻陷的 npm 发布 token,擅自发布了 cline@2.3.0(GHSA-9ppg-jx86-fqw7)。它和正常版本 2.2.3 的差异,仅仅是 package.json 里多了一行 postinstall 脚本,该脚本全局安装了一个无关的包。这个恶意版本按太平洋时间从凌晨 3 点 26 分存活到上午 11 点 30 分,大约 8 个小时(SafeDep 的分析,2026 年 2 月 18 日)。

这里有几点需要如实说明。

  • 被安装的包是合法的,并非恶意软件,没有窃取凭证或植入后门。SafeDep 的判断是,这更接近一次概念验证(proof of concept) — 展示的是可行性,而不是真实的 payload。GHSA 给出的严重程度评级也是 Low。
  • 不过,同一个 postinstall 钩子,同样可以拿来投递凭证窃取程序或反弹 shell。payload 是温和的,但机制已经被证明可行
  • token 被盗的确切路径没有公开证实。SafeDep 明确这样写。Khan 本人在更新里提到,他自己的 PoC 是在镜像仓库上跑的,不是目标仓库,他推测是另一名行为者找到了那份 PoC,直接攻击 Cline 拿到了凭证。
  • 受影响的安装数量,GHSA 里没有给出具体数字。这里我不会凭空编一个。

教训依然很清楚。轮换不会在你以为窗口已经关闭的那一刻就结束。 一个把审计做到 2 月 9 日、确认“干净”的组织,还是在 2 月 17 日看到了一次发布流出。Cline 后来吊销了 token,把 npm 发布迁移到了 OIDC provenance,彻底消除了长期静态 token 这个攻击面。这才是真正的修复 — 不是重新签发 token,而是把 token 这件事本身去掉。

同一款产品,另一扇门 — 不靠注入也能被攻破

公平起见,还得补一句。同一个 action 还有一个单独披露的漏洞,走的是不是提示注入的路径,却抵达了同样的结果。GHSA-8q5r-mmjf-575q / CVE-2026-47751(2026 年 5 月 20 日披露,严重程度 medium,影响 1.0.74 以下版本,CWE-78 和 CWE-200,由 HackerOne 的 reptou 报告)。

公告里描述的成因是三者的组合 — 检出 PR 的 head 分支(攻击者控制的内容)、通过默认配置源读取工作目录下的 .mcp.json、以及一个会无条件启用全部项目 MCP server 的设置处于开启状态。结果是,开了一个带恶意 .mcp.json 的 PR 的攻击者,能在 runner 上执行任意代码;而只要有特权用户在那个 PR 上触发这个 action,工作流里的密钥(API key 和 token)就可能泄露。

这里没有任何东西骗过模型。是智能体的配置本身处在攻击者的控制之下。而成因的形状,和前面那起事件一模一样 — 三个决定分别看都合理(检出 PR 是审查所必须的,读配置文件也是,打开项目 MCP server 也是),缺陷出现在三者相遇的地方。

这是第三个教训。只盯着注入防御,会把整整一类问题漏掉。不可信的仓库内容,不只以 prompt 的形式进来,也会以配置、依赖、构建脚本的形式进来。 智能体读到的一切都是输入,而配置文件是其中最有力的那种输入。

Anthropic 实际部署的防御

RyotaK 评价 Anthropic 的响应非常迅速。时间线是这样的 — 2026 年 1 月 12 日报告权限绕过,1 月 16 日修复(四天),1 月 17 日报告配置错误,2 月到 4 月经过多轮额外缓解和后续绕过,6 月 1 日披露。配置错误的问题在 Claude Code GitHub Actions v1.0.94 中处理完毕。Anthropic 按 CVSS v4.0 把这些问题评为 7.8,并支付了赏金(3,800 美元,外加 1,000 美元的绕过奖励)。研究者的文章里没有给出这个问题的 CVE 或 GHSA 编号,笔者也没有找到 — 后面提到的 CVE-2026-47751 是同一个 action 里另一个缺陷。

按性质把部署的防御分类,大致是这样。

修正了真正边界的措施。 GitHub App 默认被阻止触发工作流(修复提交在 agent mode 的 prepare 阶段加入了 checkHumanActor 调用)。并且不再处理触发之后被编辑过的 issue 或评论,切断了工作流合成攻击。这两处都是把信任边界本身挪动了位置的修复。

关闭了通道的措施。 默认禁用了工作流运行的 summary 区块,去掉了一条公开的泄露路径。

放慢了速度的措施。 剩下的都属于这一类。从 Claude Code 派生出的子进程里清除环境变量,并给 gh 命令套上一个自定义包装器来校验参数。这个包装器为什么必要,是这篇文章里最有价值的细节 — gh issue view 怎么看都是一条读取 issue 的、纯粹只读的命令。但 gh CLI 会把 URL 作为位置参数接收。也就是说,只要注入能让它把密钥值塞进这个 URL 里发出去,“只读”命令就直接变成了泄露通道。包装器通过检查 issue 编号是否为单个数字来堵住这一点。

这是第二个教训。工具的泄露能力来自它的参数表面,而不是它的名字。 一个能接受任意 URL 或任意路径的工具,不管名字听起来多像 read/list/view,本质上都是对外通信的手段。按工具名字来编写允许列表,会漏掉这一点。

诚实的局限 — 厂商文档自己写下的东西

写这篇文章真正的原因,从这里开始。厂商文档通常会夸大自己的防御,但 claude-code-action 的安全文档没有这么做。所以有值得引用的内容。

关于环境变量清除,文档是这么写的 — 这是一种尽力而为(best-effort)的清除,在有 bubblewrap 的 Linux runner 上会额外加上 PID 命名空间隔离,但“这降低但不能消除提示注入的风险”。因此建议把工作流权限保持在最小,并校验所有输出。

关于静态 token,写得更具体 — 不要使用个人访问令牌,静态 token 在多次运行之间不会轮换,因此“可能通过提示注入,随着时间的推移被部分或完全恢复”。紧接着那句话是点睛之笔 — 限制允许的工具“能降低恢复的速度,但未必能消除风险”。

这句话不能一带而过。它没有说防御能挡住泄露,而是说它放慢了泄露的速度。也就是说,这里的模型不是一道二元的边界,而是一个泄漏率。只要 token 活得够久、攻击者试得够多次,最终什么都会漏出去。这就是为什么处方不是“守住 token”,而是“用寿命短的 token”。

内容清洗也是同样的道理。action 会剥离 HTML 注释、不可见字符、Markdown 图片的 alt 文本、隐藏的 HTML 属性、HTML 实体。而文档紧接着补了一句 — “新的绕过技巧可能会出现”。所以它建议,在 Claude 处理之前,先人工审查来自外部贡献者的原始输入。

机器人屏蔽这一环,也把残留风险写清楚了。通过 allowed_bots 允许的机器人,不会被检查仓库权限。 在公开仓库上,任何外部主体 — 包括任何人创建的 GitHub App — 都能触发开 issue、评论、审查 PR 之类的事件,如果工作流正在监听这些事件,而 allowed_bots 设成了 *,这样的 App 就能用自己控制的 prompt 来调用这个 action。也就是说,原本的漏洞在默认设置下是关闭的,但一个选项就能把它重新打开。

而研究者自己的脚注,把这一切都浓缩成了一句话。Claude Code 缓解了对环境变量伪文件的简单读取,但这种缓解“是提高门槛的纵深防御,不是安全边界,更精巧的泄露手法依然可行”。

还有一个被测量出来的局限。RyotaK 说,截至撰文时,他已经向 Anthropic 报告了大约 50 个绕过 Claude Code 权限系统、执行任意命令的漏洞。他的结论毫不委婉 — Claude Code 周边的权限模型,还没有牢固到能安全处理不可信输入的程度。所以他建议,不管权限收得多窄,都干脆不要用 allowed_non_write_users

这个数字得准确地读。50 并不意味着“Claude Code 格外糟糕”。它意味着,在一个被积极研究、又能被快速修复的产品上,一名研究者独自一人就找出了这么多。架在命令解析器之上的权限系统,只有在解析器完美无缺时才成立,而 shell 命令解析恰恰是那种永远不会变得完美的问题。Willison 的那句话依然成立 — 我们至今仍不知道该如何 100% 可靠地挡住这个问题。

那么,做智能体的人该做什么

从这起事件里能真正带走的设计原则,梳理出来是这些。

1. 用经过验证的属性判断信任,而不是用主体的外形。 字符串后缀、用户名模式、header 值,这些都是攻击者可以自己挑的。像 checkHumanActor 这样去向签发方求证的检查,才是真的。而且这种检查必须出现在所有触发路径上 — 只存在于一条路径上的防御,等于没有防御。

2. 不要把密钥交给处理不可信输入的工作流。 这是最靠谱的一招。假设你挡不住注入,转而去构造一个“没什么可漏”的状态。就连 Anthropic API key 本身也算敏感信息的这个提醒,正是从这里来的。

3. 工具允许列表要按参数表面来编写,而不是按名字。 gh issue view 能变成泄露通道这件事,证明了这条原则。一个能接受任意 URL、任意路径、任意命令的参数,就是对外通信的手段。Khan 给 Cline 的建议也是同一件事 — 不要把 Bash、Write、Edit 交给分类工作流。

4. 用组合的眼光审查工作流,而不是逐个审查。 一个宽松的工作流和一个严格的工作流放在同一个仓库里,攻击者就会把两者拼接起来。缓存、issue 状态、构建产物 — 工作流之间共享的一切状态,都是信任边界。Khan 的一条建议精准地指出了这点 — 拥有生产部署密钥的工作流,不要去消费缓存。在发布构建上,完整性比省下几分钟更重要。

5. 确认非生产凭证是不是其实就是生产凭证。 Cline 那起事件的核心正在这里。如果注册表把 token 绑定在发布者身上,nightly token 就是生产 token。要么把命名空间分开,要么干脆去掉静态 token,改用 OIDC provenance。

6. 把防御建模成泄漏率。 认真对待厂商文档写的“降低恢复速度”这句话,设计就会随之改变。纵深防御提高的是攻击成本,不是让攻击变得不可能。所以与之配套的对策是缩短寿命、缩小爆炸半径 — 短寿命的 token、范围窄的权限,以及日志。RyotaK 建议读者去工作流运行日志里查找入侵痕迹,原因也在这里。

结语

这起事件之所以值得关注,不是因为出现了新的攻击技巧。恰恰相反,这里的一切都是老问题 — 有缺陷的信任检查、带内信令、黑名单的失败、对凭证范围的误解、工作流的组合。新的地方在于,把这些串成一条线的,是自然语言。issue 正文变成命令的那一刻起,任何能开 issue 的人,都成了潜在的 CI 执行权限拥有者。

Anthropic 的响应又快又认真(四天修复,记录在案的残留风险,支付了赏金)。Cline 的响应慢,而这个延迟直接导致了一次真实的未经授权发布。这两种对比说明的是流程的重要性,而不是哪一方“解决”了注入问题。没有人解决它。 厂商文档自己这么写,研究者用 50 个发现证明了这一点,而与此同时,一次真实的入侵也确实发生了。

所以,如果你现在正在把智能体接进 CI,更好的做法是换一个问题来问。不是“怎么防住注入”,而是“假设注入会成功,这个 runner 上什么东西可能会漏出去”。第二个问题是有答案的。第一个问题,截至 2026 年 7 月,还没有人能回答。

参考资料

현재 단락 (1/95)

2026 年 6 月 1 日,GMO Flatt Security 的 RyotaK 公开了 [Poisoning Claude Code: One GitHub Issue to Break the...

작성 글자: 0원문 글자: 11,700작성 단락: 0/95