TanStack npm 供应链攻击复盘:Pwn Request + 缓存投毒 + OIDC 内存提取的完整攻击链

2026 年 5 月 11 日 19:20–19:26 UTC,攻击者向 42 个 @tanstack/* npm 包发布 84 个恶意版本。攻击链组合利用了 GitHub Actions 的三个机制——pull_request_target Pwn Request、Actions 缓存投毒、以及从 runner 内存提取 OIDC token——在不窃取任何 npm token 的情况下完成了伪装发布。

TanStack npm 供应链攻击

恶意版本在约 20 分钟内被 StepSecurity 研究员 ashishkurmi 发现并公开报告。TanStack 已废弃全部受影响版本,npm 安全团队已介入移除 tarball。

影响范围

  • 42 个包、84 个恶意版本(每个包两个版本,间隔约 6 分钟发布)
  • 受影响:@tanstack/router 全系列子包
  • 确认未受影响:@tanstack/query*@tanstack/table*@tanstack/form*@tanstack/virtual*@tanstack/store@tanstack/start(元包)

完整受影响包列表见 tracking issue #7383

攻击链拆解

第一步:Pwn Request

2026-05-10 17:16 UTC,攻击者 fork TanStack/router 仓库,命名为 zblgg/configuration(刻意改名以规避 fork 列表搜索)。

次日 10:49 UTC,攻击者以 zblgg 身份向 TanStack/router 提交 PR #7378,标题"WIP: simplify history build"。

PR 中包含一个 3 万行的混淆 JS 文件 packages/history/vite_setup.mjs,提交者身份伪造为 [email protected]

关键点:TanStack 的 bundle-size.yml 使用 pull_request_target 触发,PR 自动进入 CI 运行,无需维护者批准。而使用 pull_request 触发的 pr.yml 被首次贡献者审核机制正确拦截。

攻击者随后多次 force-push,每次都触发新的 pull_request_target 运行,反复执行恶意代码。

第二步:GitHub Actions 缓存投毒

恶意代码在 CI 运行期间向 pnpm store 目录写入污染数据。缓存 key 为 Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')},与正式发布流程 release.yml 使用的完全一致。

当 benchmark-pr job 结束时,actions/cache@v5 的 post-step 将被污染的 pnpm store 保存到该 key。下一次 release.yml 在主分支运行时,Setup Tools 步骤会恢复这个被污染的缓存。

这是一个跨 fork↔base 信任边界的缓存投毒攻击。

第三步:OIDC Token 内存提取

release.yml 声明了 id-token: write(npm OIDC trusted publishing 需要)。当被污染的 pnpm store 在 runner 上恢复后,攻击者控制的二进制文件被调入执行:

  1. 通过 /proc/*/cmdline 定位 GitHub Actions Runner.Worker 进程
  2. 读取 /proc/<pid>/maps/proc/<pid>/mem 导出 runner 内存
  3. 从内存中提取 OIDC token(runner 在 id-token: write 设置时惰性生成)
  4. 用该 token 直接 POST 到 registry.npmjs.org,绕过 workflow 的 Publish Packages 步骤

npm 收到的发布请求携带合法的 OIDC trusted-publisher binding(TanStack/router release.yml@refs/heads/main),因此通过了验证。但发布来自恶意代码在 test/cleanup 阶段的执行,而非正常的 Publish Packages 步骤。

恶意载荷分析

安装受影响版本后,npm install 解析恶意 optionalDependencies,从 fork 网络获取孤儿 payload commit,执行约 2.3MB 的混淆脚本 router_init.js

该脚本具备三大能力:

凭据窃取: AWS IMDS / Secrets Manager、GCP metadata、Kubernetes service-account token、HashiCorp Vault token、~/.npmrc、GitHub token(环境变量、gh CLI、.git-credentials)、SSH 私钥。

加密外泄: 通过 Session/Oxen messenger 文件上传网络(filev2.getsession.orgseed{1,2,3}.getsession.org)外泄。端到端加密、无攻击者控制的 C2 服务器,只能通过域名/IP 级别网络阻断防御。

自我繁殖: 枚举受害者在 npm 上维护的其他包(registry.npmjs.org/-/v1/search?text=maintainer:<user>),自动注入并重新发布恶意版本。一个开发者中招,可能波及整个维护链。

时间线

时间 (UTC)事件
05-10 17:16攻击者创建 fork zblgg/configuration
05-10 23:29恶意 commit 提交(伪造 claude 身份)
05-11 ~10:49PR #7378 提交,触发 pull_request_target CI
05-11 11:01–11:11多次 force-push,反复触发 CI
05-11 19:20:39第一批恶意版本发布(42 个包)
05-11 19:26第二批恶意版本发布
05-11 ~19:40StepSecurity 研究员发现并报告
05-11 当天全部版本废弃,npm 安全团队介入

响应与加固

TanStack 在事件后采取了以下措施:

  1. 废弃全部 84 个恶意版本,联系 npm 安全团队移除 tarball
  2. 清除被污染的 GitHub Actions 缓存条目
  3. 合并加固 PR(#6773):重构 bundle-size.yml,添加 repository-owner guard,pin 第三方 action 引用
  4. 发布完整 postmortem,公开时间线和技术细节

GitHub Security Advisory:GHSA-g7cv-rxg3-hmpx

对开源生态的启示

OIDC trusted-publishing 作为 npm 与 GitHub 的信任桥梁,一旦 Actions 层被攻破,整个发布链的信任模型断裂。这次事件暴露了几个结构性问题:

  1. pull_request_target 的信任边界需要更严格的隔离——fork 代码不应拥有写入 base 仓库缓存的权限
  2. Actions 缓存跨 fork↔base 的共享机制是一个系统性风险点
  3. Runner 进程内存中 OIDC token 的安全性依赖 Linux /proc 权限模型,容器化环境并非天然安全
  4. pull_request_target 的防护和第三方 action 的 pin 策略,已成为开源项目 CI 加固的基本要求

据 Socket 追踪,这起事件属于 "Mini Shai-Hulud" 系列活动的一部分,已有多个 npm 和 PyPI 包受害。

自查建议: 如果你在 2026-05-11 当天安装过 @tanstack/router* 相关包,应将安装主机视为可能被入侵,轮换 AWS、GCP、Kubernetes、Vault、GitHub、npm、SSH 等全部可达凭据。

来源:TanStack Blog · Socket Blog

相关推荐