84个 npm 包遭供应链投毒攻击解析

导语:5月11日 UTC 19:20,npm 仓库凭空多出 84 个恶意版本,涵盖 42 个 @tanstack/* 包——而且全部是通过 TanStack(栈式全家桶)自己的 CI/CD 管道合法签名的。这不是简单的凭据被盗,而是一条精妙的攻击链:Pwn Request(权限获取请求)→ 缓存投毒 → OIDC(开放身份验证)凭据内存提取 → 直接发布。攻击者在合法流水线眼皮底下完成了这一切,Socket(插座安全)在 6 分钟内标记了所有恶意版本,但 npm 的未发布限制政策让清除工作被拖延了数小时。


事件概览


北京时间 5 月 12 日凌晨,Socket(插座安全)官方账号率先披露:84 个恶意 npm 包版本已发布,攻击者将其命名为 “Mini Shai-Hulud(沙丘之蝎迷你版)”,与 2025 年秋季以来的多波 Shai-Hulud(沙丘之蝎)系列供应链攻击同属一个系列,此次攻击归因于威胁组织 TeamPCP(PCP团队)

TanStack(栈式全家桶)官方于事发后紧急发布复盘报告,确认受影响版本中每一个都通过了有效的 SLSA(软件供应链级别)构建认证签名——这是首例携带有有效 SLSA Level 3(SLSA三级签名)签名的恶意 npm 包的公开记录。攻击者伪造的不是签名,而是整个构建过程本身。

关键数字

  • 42 个包84 个恶意版本(每个包 2 个,间隔约 6 分钟分两批发布)
  • 169+ 个包 受波及(含 @mistralai、@uipath、@squawk 等)
  • 12.7 万次/周 下载量的 @tanstack/react-router 首当其冲
  • 20 分钟内 外部研究员 carlini(卡里尼)发现并提交完整技术报告
  • 6 分钟 Socket(插座安全)完成所有恶意版本标记

攻击时间线:一条精密的 CI/CD 劫持路线


攻击者没有从外部窃取凭据。他们利用的是 CI/CD 管道本身的三个信任漏洞,将三条攻击向量串联成一条完整链路。

第一步:准备阶段(5月10日 ~5月11日上午)

攻击者在 GitHub(盖ithub)上注册账户 zblgg(ID 127806521),将 TanStack/router 仓库 fork 并故意重命名为 zblgg/configuration 以规避 fork 列表搜索。随后提交一个伪造身份的 commit,冒充 “claude@users.noreply.github.com”(Anthropic(anthro pik) Claude 的 GitHub 无回复地址),注入约 3 万行的捆绑 JS 载荷,并标记 [skip ci] 以躲避触发 CI。

5月11日 10:49 UTC,攻击者以此 commit 为基础向 TanStack/router#main 提交 PR #7378,标题为 “WIP: simplify history build”。这个看似正常的 PR,恰好触发了 bundle-size.yml 的 pull_request_target 工作流——该工作流直接检出并执行了来自 fork 的代码。

这就是业界熟知的 “Pwn Request(权限获取请求)” 模式,安全研究员 Adnan Khan(阿德南·汗)早在 2024 年 5 月就已公开发布相关研究报告。

第二步:缓存投毒(5月11日 11:01-11:29 UTC)

fork 代码执行时,它没有立即窃取数据,而是做了更隐蔽的事:将恶意 pnpm store 写入缓存,缓存键精确匹配 production release.yml 的计算逻辑Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')})。

缓存于 11:29 UTC 成功写入 1.1 GB 的投毒条目,并持续潜伏了将近 8 小时

bundle-size.yml 的作者尝试做了权限分离——将 benchmark 作业注释为”仅读权限”——但这道防线形同虚设:actions/cache@v5 的缓存写入不依赖 GITHUB_TOKEN,而是使用 runner 内部 token,permissions: contents: read 的配置完全不起作用。

第三步:发布恶意版本(5月11日 19:15-19:26 UTC)

19:15 UTC,一次合法的 PR 合并触发了 release.yml。缓存恢复后,攻击者控制的可执行文件在构建阶段被调用,从 GitHub(盖ithub)Actions runner 进程内存中提取 OIDC(开放身份验证)token,直接向 npm registry 发起发布请求,绕过了工作流中本应执行的发布审查步骤

整个过程在 6 分钟内完成了两批次的 84 个包发布,两批均通过合法 OIDC(开放身份验证)身份签名,附带有效 SLSA(软件供应链级别)证明。而 release.yml 报告的状态均为 “failure”——发布是通过恶意代码旁路完成的,根本不在正常发布步骤内。


受影响包全貌


@tanstack 生态(42 包 84 版本)

首当其冲的是 @tanstack/react-router(每周下载量 1270 万+)。受影响版本包括 1.169.5/1.169.8(Router 系列)和 1.167.68/1.167.71(Start 系列)等。

核心路由包全系中招:@tanstack/vue-router、@tanstack/solid-router、@tanstack/router-core、@tanstack/router-plugin、@tanstack/react-start、@tanstack/solid-start、@tanstack/vue-start 等。但确认安全的包也不少:@tanstack/query*、@tanstack/table*、@tanstack/form*、@tanstack/virtual*、@tanstack/store。

次生受害者(自传播机制)

攻击载荷内置了自传播逻辑:枚举受害者 npm scope 内的所有包,用相同的 OIDC(开放身份验证)凭据重新发布被注入的版本。数小时内,攻击波蔓延到:

  • @mistralai:Mistral AI(迷雾 AI)官方 TypeScript 客户端,版本 2.2.2-2.2.4 受影响
  • @uipath:UiPath(优路径)自动化平台 40+ 个 npm 包
  • @squawk:19 个航空数据包
  • 其他零散包:safe-action、cmux-agent-mcp、nextmove-mcp 等

截至发稿,至少 170+ 个 npm 包已在各安全厂商数据库中被标记。


恶意载荷深度剖析:router_init.js


每个受损 tarball 的根目录都被悄悄植入了约 2.3 MB 的混淆 JavaScript 文件 router_init.js——它不在 package.json 的 files 字段中,这意味着这个文件是构建过程之外被注入的,任何 tarball 完整性校验都能立刻发现异常。

三层混淆

payload 采用三层保护,Upwind Security(劲风安全)逆向后还原为 221,771 行可读代码:

Layer 1:标准 JavaScript 混淆器——字符串数组轮转自调用 + _0x253b 分发函数被调用 2864 次。所有字符串字面量替换为数组查找。

Layer 2:逐字节 Fisher-Yates(费舍尔-耶茨)替换密码,主密钥硬编码,通过 PBKDF2-SHA256(20 万次迭代)派生。解密后暴露出 C2 域名、凭据路径、内部分区名 “EveryBoiWeBuildIsAWormyBoi”

Layer 3:11 个 AES-256-GCM 加密载荷,gzip 压缩,需 Bun 运行时解密。Unit 42(42小队)确认相同的 PRNG 种子出现在 SAP、Bitwarden 和本次 TanStack(栈式全家桶)攻击中,证明三波攻击来自同一代码库。

凭据收割清单

payload 启动后首先完成 守护进程化——通过 fork+unref 方式完全脱离终端会话,静默后台运行。然后系统性扫描以下凭据来源:

  • AWS(亚马逊云):IMDSv2 元数据服务、Secrets Manager
  • GCP(谷歌云):元数据端点
  • Azure(微软云):服务主体凭据
  • Kubernetes(K8s):Service Account Token
  • HashiCorp Vault(哈希公司保险库):本地 token 缓存
  • GitHub(盖ithub):环境变量、gh CLI、.git-credentials
  • npm~/.npmrc 中的 auth token
  • SSH~/.ssh/ 私钥

三路 C2 泄露通道

  • Typosquat 域名git-tanstack.com——故意拼写与 tanstack.com 接近
  • Session P2P 网络:通过 *.getsession.org 加密传输,端对端加密,无法通过 IP/域名封堵
  • GitHub(盖ithub)API 暗桩:用窃取的 token 创建带有 “Shai-Hulud: Here We Go Again” 描述的石器主题 repo

持久化与破坏机制

发现有效的 ghp_/gho_ GitHub token 后,payload 会安装一个 gh-token-monitor 守护进程(macOS LaunchAgent / Linux systemd),每 60 秒轮询一次 GitHub。一旦 token 被吊销,守护进程触发 rm -rf ~/——在删除前 24 小时自动退出。这是典型的** retaliatory wiper** 策略。

值得注意的是:发现俄语区域设置会立即退出,不泄露数据——攻击者对特定地域做了精确规避。


开发者自查指南


第一关:排查安装记录

检查 package-lock.jsonpnpm-lock.yamlyarn.lock 中是否包含以下版本:

@tanstack/*: 1.161.9, 1.161.12, 1.166.x, 1.167.x, 1.168.x, 1.169.x
@mistralai/mistralai: 2.2.2, 2.2.3, 2.2.4

最简单的全局扫描命令:

# npm
npm ls @tanstack/react-router @tanstack/router-core @tanstack/history

# pnpm
pnpm list @tanstack/*

# yarn
yarn list @tanstack/*

第二关:查找恶意文件

# 检查项目中是否存在 router_init.js
find . -name "router_init.js" 2>/dev/null

# 检查 node_modules 中的可疑 setup.mjs
find . -path "*/node_modules/*/setup.mjs" 2>/dev/null

第三关:排查持久化守护

# Linux 检查 systemd
ls ~/.config/systemd/user/gh-token-monitor.service 2>/dev/null

# macOS 检查 LaunchAgent
ls ~/Library/LaunchAgents/com.user.gh-token-monitor.plist 2>/dev/null

第四关:轮换凭据

如果曾在 2026-05-11 安装过受影响版本,立即轮换以下凭据

凭据类型
说明
GitHub Token
ghp_/gho_ 系列 PAT/OAuth
npm Token
~/.npmrc 中的 authToken
AWS 凭据
IMDS、Secrets Manager 可访问的 IAM 角色
GCP 凭据
metadata 服务器可访问的服务账号
Kubernetes
Service Account Token
HashiCorp Vault
本地缓存的 token
SSH 私钥
~/.ssh/ 下所有私钥

第五关:封堵 C2

在防火墙/代理层封堵以下域名和 IP:

git-tanstack.com
*.getsession.org
83.142.209.194

复盘与教训


这场攻防的讽刺之处

整场攻击中,npm 仓库从未被攻破。npm 的凭据从未被盗。攻击者使用的 OIDC(开放身份验证)token 是 TanStack(栈式全家桶)自己合法发布的,SLSA(软件供应链级别)签名的验证也全部通过。这不是凭据被盗,而是信任链被拆解后逐段利用

六个值得记录的问题

一、内部告警缺失。 TanStack(栈式全家桶)在复盘报告中承认:”我们从第三方那里才知道被入侵,没有内部告警机制。” 这是大多数企业供应链安全的盲区——监控自己的发布行为本身,比监控外部威胁情报更难落地。

二、pull_request_target 未经审计。 TanStack(栈式全家桶)的 bundle-size.yml 使用了 pull_request_target 但从未被审计,这是 2024 年以来公开研究的已知高危模式,仍然大量存在于开源项目中。

三、第三方 Action 浮点引用。 使用 @v6.0.2 或 @main 引用第三方 Action,意味着任何上游修改都会自动进入你的流水线。唯一解法:锁定到 commit SHA。

四、npm 不可取消发布政策。 npm “存在依赖包时禁止取消发布” 的政策让清除工作了 10 小时以上,恶意 tarball 在这段时间内依然可安装。

五、OIDC 可信发布无法按发布来源审核。 OIDC(开放身份验证)凭据一旦绑定,工作流内任何代码路径都可以发布。TanStack(栈式全家桶)选择添加来源验证或回退短生命周期经典 token 加人工审核。

六、SLSA 证明的盲点。 SLSA(软件供应链级别)证明验证的是”构建过程本身”,不验证”构建的代码是否安全”——这在 Shai-Hulud(沙丘之蝎)攻击中首次被实战证明。有了合法 SLSA 证明的恶意包在 npm registry 上 indistinguishable(无法区分)于正常包。


84 个包,两批发布,六分钟。TanStack(栈式全家桶)的复盘报告写得很诚实:他们从第三方那里学到了这个教训。但代价是所有开发者的机器都成了潜在的攻击目标——至少在那 20 分钟内,任何 npm install @tanstack/react-router@1.169.5 的人都在下载一个经过合法签名的恶意包。供应链攻击从来不是技术问题,而是信任假设出了问题。

版权声明:本文由华盟网原创发布,保留所有权利。


👇 点击阅读原文,访问我的网站


© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容