事故复盘:TanStack npm 供应链入侵事件
摘要
本文详细复盘了针对 TanStack npm 包的供应链攻击事件,涉及缓存投毒、OIDC 令牌提取及凭证窃取恶意软件。所有受影响版本均已弃用;建议用户轮换凭证。
<p><a href="https://lobste.rs/s/d8iunk/postmortem_tanstack_npm_supply_chain">评论</a></p>
查看缓存全文
缓存时间: 2026/05/12 13:19
# 事后分析:TanStack npm 供应链攻击事件 | TanStack 博客 来源:https://tanstack.com/blog/npm-supply-chain-compromise-postmortem *作者:Tanner Linsley 于 2026 年 5 月 11 日发布。***最后更新:** 2026-05-11
2026 年 5 月 11 日 19:20 至 19:26 UTC,一名攻击者发布了 42 个 `@tanstack/*` npm 包中的 84 个恶意版本。攻击手法结合了:`pull_request_target` “Pwn Request”模式、跨越 fork↔base 信任边界的 GitHub Actions 缓存投毒,以及从 GitHub Actions runner 进程内存中提取 OIDC 令牌。没有 npm 令牌被盗,并且 npm 发布工作流本身未被攻破。
恶意版本在发布后约 20 分钟内被外部研究员**ashishkurmi**(为 **stepsecurity** 工作)公开检测到。所有受影响的版本均已废弃;我们已联系 npm 安全团队以从注册表中移除 tarball。目前没有证据表明 npm 凭证被盗,但我们**强烈建议**任何在 2026-05-11 安装了受影响版本的人员轮换从安装主机可触及的 AWS、GCP、Kubernetes、Vault、GitHub、npm 和 SSH 凭证。
**跟踪 issue:**TanStack/router#7383 (https://github.com/TanStack/router/issues/7383)
**GitHub 安全公告:**GHSA-g7cv-rxg3-hmpx (https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx)
### 受影响的包
42 个包,84 个版本(每个包两个版本,发布间隔约 6 分钟)。完整表格见跟踪 issue。已确认干净的系列:`@tanstack/query*`、`@tanstack/table*`、`@tanstack/form*`、`@tanstack/virtual*`、`@tanstack/store`、`@tanstack/start`(元包,而非 `@tanstack/start-*`)。
### 恶意软件行为
当开发者或 CI 环境对任何受影响版本执行 `npm install`、`pnpm install` 或 `yarn install` 时,npm 解析恶意的 `optionalDependencies` 条目,从 fork 网络中获取孤立的 payload 提交,运行其 `prepare` 生命周期脚本,并执行一个约 2.3 MB 的被混淆的 `router_init.js`,该脚本被偷运进受影响的 tarball 中。该脚本:
- 从常见位置收集凭证:AWS IMDS / Secrets Manager、GCP 元数据、Kubernetes 服务账户令牌、Vault 令牌、`~/.npmrc`、GitHub 令牌(环境变量、`gh` CLI、`.git-credentials`)、SSH 私钥
- 通过 Session/Oxen 信使文件上传网络(`filev2.getsession.org`、`seed{1,2,3}.getsession.org`)外泄数据——端到端加密,无攻击者控制的 C2,因此基于 IP/域名的阻断是唯一的网络缓解措施
- 自我传播:通过 `registry.npmjs.org/-/v1/search?text=maintainer:` 枚举受害者维护的其他包,并使用相同注入方式重新发布它们
由于 payload 作为 npm install 生命周期的一部分运行,任何在 2026-05-11 安装了受影响版本的人员必须将安装主机视为潜在受感染。
---
所有时间为 UTC。本地时间戳来自 GitHub API 和 npm 注册表。
### 攻击前(缓存投毒阶段)
| 时间 | 事件 |
| --- | --- |
| 2026-05-10 17:16 | 攻击者创建 fork `github.com/zblgg/configuration` (https://github.com/zblgg/configuration)(为 TanStack/router 的 fork,故意重命名以逃避 fork 列表搜索) |
| 2026-05-10 23:29 | 恶意提交 `65bf499d16a5e8d25ba95d69ec9790a6dd4a1f14` 在 fork 上由虚构身份 `claude` 创作。添加 `packages/history/vite_setup.mjs`(约 30,000 行的打包 JS payload)。提交消息前缀为 `[skip ci]` 以在 push 事件中抑制 CI |
| 2026-05-11 ~10:49 | PR #7378 由 `zblgg` 针对 `TanStack/router#main` 开启,标题为“WIP: simplify history build” |
| 2026-05-11 10:49 起 | `bundle-size.yml` 和 `labeler.yml`(均使用 `pull_request_target`)自动为该 PR 运行——无需首次贡献者批准,因为 `pull_request_target` 绕过了该限制。`pr.yml`(使用 `pull_request`)未运行,因等待从未到来的批准而被阻止 |
| 2026-05-11 11:01–11:11 | `zblgg` 多次 force-push 到 PR head,每次都触发更多 `pull_request_target` 运行 |
| 2026-05-11 11:11 | Force-push 将 `65bf499d`(恶意提交)落到 PR head 上。`bundle-size.yml` 的 `benchmark-pr` job 签出 `refs/pull/7378/merge`,运行 `pnpm install` + `pnpm nx run @benchmarks/bundle-size:build`——这会执行 `vite_setup.mjs` |
| 2026-05-11 11:29 | 缓存条目 `Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11` (1.1 GB)保存到 GitHub Actions 缓存,针对 `TanStack/router`,作用域为 `refs/heads/main`——键名与 `release.yml` 在下一次 push 到 main 时将查找的键名匹配 |
| 2026-05-11 11:31 | 攻击者将 PR force-push 回当前 main HEAD(`b1c061af`),使可见的 PR 变为 0 文件无操作。PR 在同一分钟内关闭并删除分支。缓存投毒仍然存在 |
### 引爆(发布阶段)
| 时间 | 事件 |
| --- | --- |
| 2026-05-11 19:15 | Manuel 合并 PR #7369(Shkumbin 的 `CSS.supports` 修复)→ 推送到 main 触发 `release.yml`。工作流运行 `25613093674` 开始(19:15:44),并失败。 |
| 2026-05-11 19:20:39 | npm 注册表收到 `@tanstack/[email protected]` 及 41 个兄弟包的发布(42 个包中约 84 个版本,但只有约一半显示这一确切秒数;其余版本在第二次运行中到来)。发布通过 OIDC 可信发布者绑定认证,用于 `TanStack/router` 的 `release.yml@refs/heads/main`——但它并非来自工作流定义的 Publish Packages 步骤(该步骤因测试失败而被跳过)。它来自在测试/清理阶段运行的恶意软件,该恶意软件通过工作流的 `id-token: write` 权限生成 OIDC 令牌,并直接 POST 到 `registry.npmjs.org` |
| 2026-05-11 19:20:47 | 运行 `25613093674` 完成(状态:失败) |
| 2026-05-11 19:16 | Manuel 合并 PR #7382(jiti tsconfig paths 修复)→ 第二次推送到 main 触发 `release.yml` |
| 2026-05-11 19:16:22 | 工作流运行 `25691781302` 开始。相同的投毒缓存被恢复 |
| 2026-05-11 19:26:14 | npm 注册表收到每个包的第二个版本(`@tanstack/[email protected]` 等)。使用相同的 OIDC 机制 |
| 2026-05-11 19:26:20 | 运行 `25691781302` 完成(状态:失败) |
### 检测和响应
| 时间 | 事件 |
| --- | --- |
| 2026-05-11 ~19:50 | 外部研究员(carlini)开启 issue #7383,详细描述了恶意 `optionalDependencies` 特征和包列表(最初为 42 个中的 14 个) |
| 2026-05-11 ~19:50 | 研究员直接通知 npm 安全团队 |
| 2026-05-11 ~20:00 | Manuel 在 #7383 中确认——事件响应开始 |
| 2026-05-11 ~20:10 | Manuel 以防用户计算机受感染,移除 GitHub 上所有其他团队推送权限 |
| 2026-05-11 ~20:30 | Tanner 向 [[email protected]](mailto:[email protected]) 发送完整 IOC 列表,并要求服务端拉取 tarball。通过 npm 提交正式恶意软件报告 |
| 2026-05-11 ~21:00 | 全面的 295 个 `@tanstack/*` 包扫描确认范围:42 个包,84 个版本。Tanner 开始对所有 84 个受影响包进行 npm 废弃处理。通过 @tan_stack 和维护者在 Twitter/X/LinkedIn/Bluesky 上进行公开披露 |
| 2026-05-11 21:30 | 调查识别出 `bundle-size.yml` 的 `pull_request_target` 缓存投毒向量以及 `zblgg/configuration` fork。通过 API 清除所有 `TanStack/*` GitHub 仓库的所有缓存条目。合并加固 PR:重构 `bundle-size.yml`,添加 `repository_owner` 守卫,将第三方 action 引用固定到 SHA。发布官方 GitHub 安全公告,CVE 已申请 |
---
三个漏洞被串联利用。每个漏洞对攻击而言都是必要条件;单独一个都不足以实现。
### 1. `bundle-size.yml` 中的 `pull_request_target` “Pwn Request” 模式
`bundle-size.yml` 为 fork PR 运行 `pull_request_target`,并在该触发上下文中签出 fork 的 PR-merge ref 并运行构建:
```
on: pull_request_target: paths: ['packages/**', 'benchmarks/**'] jobs: benchmark-pr: steps: - uses: actions/[email protected] with: ref: refs/pull/${{ github.event.pull_request.number }}/merge # fork 的合并代码 - uses: TanStack/config/.github/setup@main # 间接调用 actions/cache@v5 - run: pnpm nx run @benchmarks/bundle-size:build # 执行 fork 控制的代码
```
工作流作者尝试进行信任分离(`comment-pr` job 独立于 `benchmark-pr`,YAML 中有注释说明意图保持 `benchmark-pr` “不可信,具有只读权限”)。该分离在思路正确,但忽略了两个事实:
- `actions/cache@v5` 的 post-job 保存操作不受 `permissions:` 限制。缓存写入使用 runner 内部令牌,而非工作流 `GITHUB_TOKEN`。设置 `permissions: contents: read` 不会阻止缓存突变。
- 缓存作用域是按仓库的,在 `pull_request_target` 运行(使用基础仓库的缓存作用域)和推送到 main 的运行之间共享。一个在基础仓库缓存作用域内运行的 PR 可以投毒条目,这些条目后续会被 main 上的生产工作流恢复。
### 2. 跨信任边界的 GitHub Actions 缓存投毒
恶意 `vite_setup.mjs` 专门设计为向 pnpm 存储目录写入数据,键名正是合法 `release.yml` 工作流将计算并查找的:`Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')}`。当 `benchmark-pr` job 结束时,`actions/cache@v5` 的 post-step 将(现在被投毒的)pnpm 存储保存到该确切键名。
当 `release.yml` 下一次在 push 到 main 时运行时,其 Setup Tools 步骤完全按设计恢复被投毒的条目。这是 Adnan Khan 在 2024 年记录的已知攻击类别。这不是 TanStack 特有的 bug;这是一个已知的 GitHub Actions 设计问题,需要有意识地进行缓解。
`release.yml` 声明了 `id-token: write`(这是 npm OIDC 可信发布所合法需要的)。当被投毒的 pnpm 存储被恢复到 runner 上时,攻击者控制的二进制文件现在存在于磁盘上,并在构建步骤中被调用。这些二进制文件:
- 通过 `/proc/*/cmdline` 定位 GitHub Actions `Runner.Worker` 进程
- 读取 `/proc//maps` 和 `/proc//mem` 以转储 worker 的内存
- 提取 OIDC 令牌(当设置 `id-token: write` 时,runner 会在内存中惰性生成)
- 使用该令牌直接向 `registry.npmjs.org` 进行 POST 请求认证——完全绕过工作流的 Publish Packages 步骤
这与 2025 年 3 月 `tj-actions/changed-files` 攻击中使用的内存提取技术(以及逐字逐句的 Python 脚本,带属性注释)相同。攻击者没有发明新的技战术;他们重组了已发表的研究成果。
### 为什么单独一个都不够
- 单独的 `pull_request_target` 对可信操作(打标签、评论)而言是安全的
- 单独的缓存投毒(例如,来自已被攻陷的依赖内部)需要一个独立的发布载体
- 单独的 OIDC 令牌提取需要在 runner 上已经存在代码执行能力
这个链条之所以可行,是因为每个漏洞都架起了其他漏洞所假设的信任边界:PR fork 代码跨越到基础仓库缓存,基础仓库缓存跨越到发布工作流运行时,发布工作流运行时跨越到 npm registry 写入权限。
---
### 我们如何发现
检测来自外部。`carlini` 在发布约 20 分钟后打开了 issue #7383,并提供了完整的技术分析。Tanner 在战争室启动后片刻即接到 Socket.dev 的电话,确认了情况。
### IOC 特征(供下游维护者和安全工具使用)
在任何 `@tanstack/*` 包的清单中:
```
"optionalDependencies": { "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" }
```
- 文件:`router_init.js`(约 2.3 MB,包根目录,不在 `"files"` 中)
- 缓存键:`Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11`
- 第二阶段 payload URL:`https://litter.catbox.moe/h8nc9u.js`、`https://litter.catbox.moe/7rrc6l.mjs`
- 外泄网络:`filev2.getsession.org`、`seed{1,2,3}.getsession.org`
- 伪造的提交身份:`claude`(注意:并非真实的 Anthropic Claude——虚构的 GitHub no-reply 邮箱)
- 真实攻击者账户:`zblgg`(id 127806521)、`voicproducoes`(id 269549300)
- 攻击者 fork:`github.com/zblgg/configuration` (https://github.com/zblgg/configuration)(TanStack/router 的 fork,重命名以逃避 fork 搜索)
- 孤立的 payload 提交(在 fork 网络中):`79ac49eedf774dd4b0cfa308722bc463cfe5885c`
- 执行恶意发布的工作流运行:
- github.com/TanStack/router/actions/runs/25613093674 (https://github.com/TanStack/router/actions/runs/25613093674)(尝试 4)
- github.com/TanStack/router/actions/runs/25691781302 (https://github.com/TanStack/router/actions/runs/25691781302)
---
### 哪些地方做得好
- 外部研究员在事发约 20 分钟内便注意到并以完整技术细节报告
- 维护者团队跨越多个时区立即有效协调
- 检测社区在数小时内已形成清晰的公开 IOC 模式
### 哪些地方能做得更好
- **无内部告警。** 我们是从第三方得知本次攻击的。我们需要监控自己的发布。我们将与生态系统中能够快速检测此类问题的安全研究公司密切合作,甚至可能建立内部检测能力,并进一步缩短反馈循环。
- `pull_request_target` 工作流尽管是长期已知的危险模式,但未曾审计
- 第三方 action 上的浮动引用(`@v6.0.2`、`@main`)造成了独立于本事件的持续性供应链风险
- 由于 npm 的“存在依赖者则不可取消发布”政策,几乎所有受影响包都无法取消发布。我们必须依赖 npm 安全团队服务端拉取 tarball,这增加了数小时的延迟,期间恶意 tarball 仍可被安装
- npm scope 上的 7 位维护者意味着七个独立的凭据窃取目标,享有相同的爆炸半径
- OIDC 可信发布者绑定没有逐次发布审查。一旦配置,工作流中的任何代码路径都能生成具有发布能力的令牌。我们需要要么 (a) 转向带人工审查的短期传统令牌,要么 (b) 添加来源来源验证以检测非预期工作流步骤的发布
### 我们侥幸之处
- 攻击者选择的 payload 破坏了测试,导致发布步骤(本可生成更干净的 tarball)被跳过——这意味着攻击足够喧闹而被快速检测到。一个更谨慎的攻击者若未破坏测试,可能可以在更长时间内悄然发布
- 攻击者复用了公开的技战术(逐字逐句的记忆转储脚本及属性注释),而非编写新代码——这使得 IOC 匹配更快
---
在关闭事后分析前,这些问题需要得到解答。
- `bundle-size.yml` 的 Setup Tools 步骤是否确实调用了 `actions/cache@v5`?通过阅读针对 PR #7378 的某次 `pull_request_target` 运行的 post-job 日志来验证(例如,运行 ID 25666610798)。Tanner 有权访问;需手动完成
- 初始 PR head 提交中有什么内容(在 force-push 清除之前)?GitHub 的 reflog 可能还保留着。通过 `gh api` 或 GitHub 支持团队查询
- 恶意提交是如何具体进入 fork 的 git 对象存储的——是直接通过 git 推送,还是通过 GitHub web UI 创建(这会留下审计日志条目)?
- `voicproducoes` 是真实账户还是马甲?交叉参考其活动历史
- npm 缓存是否也被投毒(6 个重复的 `linux-npm-store-*` 条目)?是否有任何条目被实际使用?
- 该攻击是否需要 Nx Cloud,还是仅用 GitHub Actions 缓存即可实施?
- 我们能否在 `TanStack/router` fork 网络中识别出包含该孤立 payload 提交的其他 fork?(若存在,清理将更棘手——每个 fork 都可能
相似文章
@tan_stack: 安全公告 —— TanStack npm 包遭遇供应链攻击,影响 42 个 @tanstack/* 包(共 84 个版本……
一次高危供应链攻击影响了 42 个 TanStack npm 包,导致云凭证和 SSH 密钥被窃取。建议用户在攻击时间窗口内安装过相关包的,立即轮换凭证并从干净的 lockfile 重新安装。
我们对 TanStack npm 供应链攻击的回应
OpenAI 回应 TanStack npm 供应链攻击,表示没有用户数据或生产系统被攻破,但有两名员工的设备受到影响,内部代码仓库中的少量凭证被窃取。
TanStack NPM 软件包遭入侵
报告显示 TanStack NPM 软件包存在安全漏洞,波及使用 TanStack Router 和 Start 框架的开发人员。
Grafana Labs GitHub仓库因TanStack npm供应链攻击遭入侵
Grafana Labs披露,一个网络犯罪团伙通过TanStack npm供应链攻击非法访问了其GitHub仓库,下载了代码库和内部数据,但未涉及客户生产系统。
@DeRonin_: 使用本指南保护你的计算机免受 NPM 攻击,这些攻击会在一次安装中窃取一切。TanStack,一个代码库使用……
本文详细介绍了针对 NPM 上 TanStack 库的供应链攻击,并提供了一份全面的指南,通过锁定依赖项发布年龄、固定版本以及对 CI/CD 流水线和 IDE 扩展进行审计,来保护开发环境的安全。