返回博客

拆解 TanStack 投毒:黑客没偷你的 npm 密码,却发布了恶意包

安全供应链攻击GitHub ActionsnpmOIDCPostmortem
hero

TL;DR

  • 2026 年 5 月 11 日,TanStack 的 42 个 npm 包被植入恶意版本,84 个版本同时上架,单是 @tanstack/react-router 每周下载量就过 1200 万。
  • 全网当天倒下 170+ 个 npm 包 + 2 个 PyPI 包,共 404 个恶意版本,CVE-2026-45321,CVSS 9.6。
  • 攻击者全程没有偷 npm 凭据。他们从 GitHub Actions runner 的进程内存里抠出 OIDC token,直接代发包。
  • OpenAI 的两台员工设备被波及,macOS 签名证书要在 6/12 之前全量轮换,ChatGPT Desktop / Codex / Codex CLI / Atlas 全得重签。
  • 这不是 npm 的事故,是 GitHub Actions 信任模型的事故。

一、案发现场

5 月 11 号 UTC,TanStack 维护者起床看到 npm registry 上多出来一堆没人发过的包版本。

@tanstack/react-router、@tanstack/react-query、@tanstack/table——这些是 React 圈装机量最大的几个库之一。每分钟全球有几千次 npm install 在拉它们。一夜之间,84 个版本同时变成了"看起来正常、跑起来偷你 AWS key"的特洛伊木马。

同一天倒下的还有 Mistral AI、UiPath、Guardrails AI——AI 工具链上的硬通货。攻击者一个团伙:TeamPCP,2025 年底冒头,专攻云原生供应链。蠕虫叫 Mini Shai-Hulud——去年那波原始 Shai-Hulud 的"迷你"版,但杀伤面更宽。

漏洞编号已经下来:CVE-2026-45321,评分 9.6。

二、攻击者要解决的问题

把恶意代码塞进 @tanstack/react-router 的官方 npm 发布,难度有几层。

最容易想到的办法是钓鱼。给维护者发个伪装邮件骗 npm token、social engineering 套出二次验证码。但 TanStack 的维护者用了硬件 key,钓鱼这条路堵死。

第二条路是偷 secret。把 GitHub 仓库的 NPM_TOKEN 偷出来,自己跑发布。但 TanStack 已经不存长期 npm token——他们用的是 GitHub OIDC,发布时由 GitHub 现场颁发短期 token 给 npm registry。这是 2024 年起被推荐的"最佳实践"。

攻击者没走任何一条传统路。他们盯上了一个所有人都默认信任的位置:CI 系统自己。

三、攻击链拆解

diagram

步骤 1:fork + orphaned commit

攻击者 fork 了 TanStack/router 仓库,然后在 fork 里推了一个 orphaned commit——跟 TanStack 主干没有共同祖先的孤立提交。

为什么要 orphaned?因为它让 GitHub 的历史检查变得复杂。CI 系统判断"这个 commit 是不是从可信源派生"时,遇到 orphaned commit 会走默认路径,而默认路径几乎没人加固。

步骤 2:开 PR 触发 pull_request_target

攻击者用 fork 开了一个 PR。这个 PR 触发了 TanStack 仓库里的 pull_request_target workflow。

这是 GitHub Actions 的经典 footgun,安全圈叫它 "Pwn Request"。pull_request_target 触发器有个特点:它跑的是 base 仓库的 workflow 定义(所以维护者觉得"安全"),但 checkout 默认拿的是 PR 来源的代码(攻击者的 fork)。

维护者写 workflow 时常忘记这一点。结果是攻击者的代码在 TanStack 主仓库的 secrets 上下文里被执行——拿到了访问仓库 secret 的能力。

步骤 3:cache poisoning,然后退场

攻击者没急着干票大的。他们做了一件看起来很无聊的事:往 GitHub Actions cache 里写了一份恶意 pnpm store。

然后关掉 PR,安静等着。

GitHub Actions 的 cache 是按仓库共享的。维护者后续合任何一个 PR 到 main,release workflow 跑起来,第一步就是从 cache 取 pnpm store——取到了被污染的那份。

几小时后,一个完全正常的 maintainer PR 被合进 main。release 自动跑起来。整个发布流程跑在被污染的 node_modules 上,但维护者完全看不出来——构建没报错,测试都过了。

步骤 4:直接抢 OIDC token

被污染的 pnpm store 里塞了一个二进制。它在 release workflow 启动后做了一件事:读 /proc//mem,直接从 GitHub Actions runner 进程的内存里抠 OIDC token。

这是关键。OIDC token 设计来代替长期 secret,理论上更安全——它只有几分钟有效期,绑定特定 audience。但这个"安全"的前提是:跟 token 同进程的所有代码都是可信的。

恶意二进制跟合法发布脚本跑在同一个 runner 上、同一个 Linux 用户、同一个进程组。读对方内存几乎没门槛。token 到手,攻击者直接调 npm registry 发了 84 个恶意版本。

全程没碰任何长期凭据。

illustration

四、爆炸半径

直接受害:42 个 @tanstack 包,84 版本,已下架但已经被大量 npm install 拉走。

二次传播更可怕。恶意脚本在受害者 CI 上跑起来,做的不止是收集——它会扫描所有能访问的环境变量,把 AWS key、Anthropic API key、Discord webhook、加密钱包助记词、SSH key 打包外发。然后蠕虫复用同一台 CI 上其他 npm 维护者的发布权限,继续往外感染包。

OpenAI 是被波及的一个。他们公告里说"两台员工设备在公司环境里受到这次攻击影响"。看似规模小,但这两台机器之一参与了 macOS 应用的签名流程。

结果是:OpenAI 必须把所有 macOS 应用的代码签名证书全部轮换。ChatGPT Desktop、Codex、Codex CLI、Atlas——四个都得重签。macOS 用户必须在 2026 年 6 月 12 日前升级到新证书签名的版本,否则 macOS 系统会直接拒绝运行。

这不是 OpenAI 第一次。一个月前的 Axios 攻击已经让他们的 macOS 签名管道挨过一次,证书刚轮换完。这次又一次。同一个团伙,同一个套路。

五、根因不在 npm

复盘到这一步,很多人第一反应是"npm 又不行了"。

错。npm 在这次事件里几乎是受害者——他们提供的短期 OIDC token 机制本身是更安全的设计。

真正出问题的是 GitHub Actions 的信任模型:

  • pull_request_target 触发器跟普通 pull_request 长得像,但安全语义完全不同,文档藏在角落,维护者踩坑率极高。
  • GitHub Actions cache 按仓库共享,没有签名校验,写入权限的判断基于"谁能跑 workflow"——而 pull_request_target 让攻击者能跑 workflow。
  • Runner 进程之间没有内存隔离。同一 workflow 里"取 cache"和"读 OIDC token"跑在同一个 Linux 进程,恶意代码读合法代码的内存零门槛。

零信任、短期凭据、最小权限——这三个安全圈讲了十年的原则,在 cache poisoning + 进程内存读取面前一起失效。短期凭据没用,因为攻击者活在那几分钟里。最小权限没用,因为他们要的就是"发包"这一个最小权限。

六、你能带走的几件事

给个人开发者:

  • 锁版本package.json 写死版本号,pnpm-lock.yaml / package-lock.json 跟到 git,每个 PR 看 lockfile diff。这次很多人中招就是因为 ^x.y.z 自动拉到了恶意版本。
  • 本地缓存定期清~/.npm~/.cache/pnpm~/.yarn/cache 这些目录如果在恶意期间被污染过,你机器上的开发环境就带毒。
  • CI runs locally 的指令不要带 --ignore-scripts=false。装一个 install-scripts-blocker 这类工具。

给团队:

  • pull_request_target 触发器永远不要 checkout PR 来源代码。如果非要,先把 PR 的 SHA 钉死,再加一层人工审批。
  • 发布步骤和测试步骤跑在不同 GitHub Environment 上,发布 environment 加 required reviewers。
  • OIDC token 加 audience 限制和路径限制——id-token: write 不要写在 workflow 顶层。
  • 用 npm provenance + sigstore,让消费方能校验包的发布来源。
  • 关键依赖加 SBOM 监控,订阅 Socket / Snyk / Aikido 这类工具的实时投毒告警。

最重要的一条认知:你的 CI 不是个跑测试的容器,是个"代你以维护者身份操作所有外部系统的特工"。它有你 npm 的发包权限、你 AWS 的部署权限、你 Slack 的发消息权限。它被攻破等于你被攻破。

我自己产品的 release 流程里这周已经做了三件事:把 pull_request_target 全部换掉、把 release 环境跟 PR 环境拆开、给发包加 required reviewer。代价是发包从 1 分钟变成 5 分钟——值。

附录:信息源

  • TanStack 官方 postmortem
  • OpenAI 官方响应公告(2026-05-13)
  • Wiz / Snyk / StepSecurity / Aikido 各家的攻击链拆解
  • CVE-2026-45321 / NVD 评估