Back openDesk Edu for a sovereign, open-source education — every vote counts.
Vote nowSave products you love by clicking the heart icon.
Essential reference for GitHub Actions and GitLab CI pipelines, including workflow syntax, runners, secrets, artifacts, and deployment patterns.
On 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 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
- 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.
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:
/proc/*/cmdline/proc/<pid>/maps and /proc/<pid>/mem to dump the worker's memoryid-token: write is set)registry.npmjs.org — bypassing the workflow's Publish Packages step entirelyThis 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.
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.
| 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 ~/.
| 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 |
For security teams scanning downstream:
"optionalDependencies": {
"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}
router_init.js (~2.3 MB, package root, not in "files")Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11filev2.getsession.org, seed{1,2,3}.getsession.orgclaude <claude@users.noreply.github.com> (fabricated, not Anthropic)zblgg (GitHub ID 127806521), voicproducoesTanStack's analysis is notable for its candour:
pull_request_target workflows had never been audited despite being a long-known dangerous pattern@v6.0.2, @main) create standing supply-chain risk independent of this incidentThe 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.
If you run pull_request_target workflows, audit for these specific conditions:
# 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
pull_request_target and main branch workflowsactions/cache@v5 post-job save is not gated by permissions: contents: read- uses: actions/cache@v5
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# ↑ Only save cache for trusted contexts
id-token: write mints a token lazily in runner memory on first useenvironment: publish # Configure environment protection rules in GitHub settings
permissions:
id-token: write
contents: read
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