TanStack Supply Chain Attack: Anatomy of a Three-Stage Breach
· ~6 min readOn 2026-05-11 between 19:20 and 19:26 UTC, an attacker published 84 malicious versions across 42 @tanstack/* npm packages. The attack required three distinct vulnerabilities, none sufficient alone. TanStack has published a thorough postmortem; this article extracts the technical details every DevOps engineer should be auditing in their own pipelines today.
The Three-Stage Attack Chain
Stage 1: pull_request_target Cache Poisoning
The attacker's entry point was a fork PR against TanStack/router. The malicious PR (#7378) opened from account zblgg exploited the pull_request_target trigger in 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
# ↑ Checks out the FORK's merged code — attacker-controlled
- uses: TanStack/config/.github/setup@main
# ↑ Transitively calls actions/cache@v5
- run: pnpm nx run @benchmarks/bundle-size:build
# ↑ Executes attacker code in the build step
pull_request_target runs with write permissions on the base repository and checks out the PR's merged code. The workflow author intended a trust split between "untrusted" PR code and read-only permissions, but actions/cache@v5's post-job save is not gated by permissions: contents: read. Cache writes use a runner-internal token. The PR's build poisoned the pnpm store under a key the production release.yml workflow would later compute and restore.
When release.yml ran on a push to main, it restored the poisoned cache — attacker-controlled binaries now on disk inside the release workflow's context.
Stage 2: OIDC Token Memory Extraction
release.yml declared id-token: write legitimately for npm OIDC trusted publishing. When the poisoned pnpm store was restored, those binaries were invoked during the build step. They then:
- Located the GitHub Actions Runner.Worker process via
/proc/*/cmdline - Read
/proc/<pid>/mapsand/proc/<pid>/memto dump the worker's memory - Extracted the OIDC token (minted lazily in memory when
id-token: writeis set) - Used the token to POST directly to
registry.npmjs.org— bypassing the workflow's Publish Packages step entirely
This is the same memory-extraction technique and verbatim Python script used in the tj-actions/changed-files compromise of March 2025. No novel tradecraft — recombination of published research.
Stage 3: Self-Propagation
The malware's prepare lifecycle script also enumerated other packages the victim maintains via registry.npmjs.org/-/v1/search?text=maintainer:<user> and republished them with the same injection. This is a worm — not just credential theft, but lateral spread through the victim's own package portfolio.
Impact
| Metric | Value |
|---|---|
| Packages affected | 42 @tanstack/* packages |
| Versions published | 84 (two per package, 6 minutes apart) |
| Detection | External researcher (ashishkurmi, StepSecurity), ~20 minutes post-publish |
| No npm tokens stolen | Attack used OIDC token minted from inside the workflow |
| Self-propagation | Malware enumerated and republished victim's other packages |
The payload installed a dead-man's switch: ~/.local/bin/gh-token-monitor.sh as a systemd user service (Linux) or LaunchAgent (macOS). It polled api.github.com/user every 60 seconds. If the stolen token was revoked while the infected machine was still online, the script ran rm -rf ~/.
Timeline
| Time (UTC) | Event |
|---|---|
| 2026-05-10 17:16 | Attacker forks TanStack/router as zblgg/configuration |
| 2026-05-10 23:29 | Malicious commit 65bf499d added to fork (30,000-line bundled payload) |
| 2026-05-11 10:49 | PR #7378 opened; pull_request_target workflows auto-trigger |
| 2026-05-11 11:11 | Force-push delivers payload commit via PR head |
| 2026-05-11 11:29 | Poisoned cache entry saved: Linux-pnpm-store-6f9233a50... |
| 2026-05-11 11:31 | PR force-pushed back to main HEAD, then closed — cache poison persists |
| 2026-05-11 19:15 | Legitimate PR #7369 merges → release.yml triggers |
| 2026-05-11 19:20:39 | First batch of 42 packages published via stolen OIDC token |
| 2026-05-11 19:20:47 | Release workflow run fails (malicious payload broke tests) |
| 2026-05-11 ~19:50 | External researcher opens issue with full IOC analysis |
| 2026-05-11 21:00 | All 84 affected versions deprecated; caches purged |
| 2026-05-11 21:30 | Hardening PR merged; GitHub Security Advisory published |
Detection IOCs
For security teams scanning downstream:
"optionalDependencies": {
"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}
- File in affected tarball:
router_init.js(~2.3 MB, package root, not in "files") - Cache key:
Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11 - Exfiltration network:
filev2.getsession.org,seed{1,2,3}.getsession.org - Forged commit identity:
claude <claude@users.noreply.github.com>(fabricated, not Anthropic) - Attacker accounts:
zblgg(GitHub ID 127806521),voicproducoes
What the Postmortem Gets Right
TanStack's analysis is notable for its candour:
- No internal alerting detected the compromise — third-party researcher reported first
pull_request_targetworkflows had never been audited despite being a long-known dangerous pattern- Floating action refs (
@v6.0.2,@main) create standing supply-chain risk independent of this incident - OIDC trusted-publisher binding has no per-publish review: once configured, any code path in the workflow can mint a publish-capable token
- npm's "no unpublish if dependents exist" policy prevented direct removal; had to rely on npm security pulling tarballs server-side
The attacker also got lucky: their payload broke tests, which caused the publish step (which would have produced cleaner-looking tarballs) to skip. A more careful attacker could have published silently for hours.
Hardening Checklist
If you run pull_request_target workflows, audit for these specific conditions:
pull_request_target
# NEVER do this — checkout of fork PR head inside a workflow with write permissions
- uses: actions/checkout@v6.0.2
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
# Instead: use github.event.pull_request.head.repo.full_name to detect cross-repo
# Or: use a separate "untrusted" job that has NO write permissions and NO cache access
GitHub Actions Cache
- Cache scope is per-repo, shared across
pull_request_targetandmainbranch workflows actions/cache@v5post-job save is not gated bypermissions: contents: read- Mitigation: use separate cache keys per trigger context, or disable cache restore entirely for untrusted triggers
- uses: actions/cache@v5
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# ↑ Only save cache for trusted contexts
OIDC Trusted Publishing
id-token: writemints a token lazily in runner memory on first use- Any attacker code running on the runner can extract it
- Mitigation: GitHub Environments with manual approval gates for publish workflows
- Further mitigation: require manual approval on npm registry side (not yet standard practice)
environment: publish # Configure environment protection rules in GitHub settings
permissions:
id-token: write
contents: read
Broader Implications
The security community has documented pull_request_target cache poisoning since 2024 (Adnan Khan, "The Monsters in Your Build Cache"). This attack demonstrates that known, documented patterns are being actively exploited at scale against high-profile open-source projects.
The chain demonstrates that OIDC trusted publishing, while an improvement over static tokens, does not prevent a compromised CI pipeline from publishing malicious packages. The second factor — npm's side — remains the gap: once an OIDC token is minted from inside a workflow, any code in that workflow can use it to publish.
The self-propagating design (enumerating and republishing the victim's other packages) represents an escalation from pure supply-chain attack to automated lateral movement. This is no longer just about stealing credentials — it's about turning your package publication infrastructure into a malware distribution network.
References