{"slug": "postmortem-tanstack-npm-supply-chain-compromise", "title": "Postmortem: TanStack npm supply-chain compromise", "summary": "On May 11, 2026, an attacker compromised the TanStack Router/Start repository and published 84 malicious npm package versions across 42 packages by exploiting a pull_request_target vulnerability, GitHub Actions cache poisoning, and runtime OIDC token extraction. The malicious packages were detected within 26 minutes by an external researcher, and all affected versions have been deprecated and removed from the npm registry. Users who installed any affected version must treat their install host as potentially compromised and rotate credentials.", "body_md": "*by Tanner Linsley on May 11, 2026.*\n\nStatus (2026-05-15): All clear ✅After a three-day full security sweep and hardening pass, we're issuing an official all-clear on TanStack repo and package security.\n\n- Only the Router/Start repo was affected — 42 monorepo packages, 2 versions each. All were deprecated within the hour and removed by npm shortly after.\n- All other TanStack repos and packages were unaffected and remain secure: Query, DB, Store, AI, Table, Form, HotKeys, Virtual, Pacer, Config, Devtools, CLI, Intent, etc.\n- Every currently-available published version of every TanStack package — Router and Start included — is safe to install.\nSee also:\n\n[Hardening TanStack After the npm Compromise]— the companion piece covering what we're changing because of this incident.\n\n*Last updated 2026-05-15 — see Changelog.*\n\nOn 2026-05-11 between 19:20 and 19:26 UTC, an attacker published 84 malicious versions across 42 @tanstack/* npm packages by combining: the pull_request_target \"Pwn Request\" pattern, GitHub Actions cache poisoning across the fork↔base trust boundary, and runtime memory extraction of an OIDC token from the GitHub Actions runner process. No npm tokens were stolen and the npm publish workflow itself was not compromised.\n\nThe malicious versions were detected publicly within 20 to 26 minutes (depending on which of the two publish batches a given version came from) by an external researcher ashishkurmi working for stepsecurity. All affected versions have been deprecated; npm security has been engaged to pull tarballs from the registry. We have no evidence of npm credentials being stolen, but we strongly recommend that anyone who installed an affected version on 2026-05-11 rotate AWS, GCP, Kubernetes, Vault, GitHub, npm, and SSH credentials reachable from the install host.\n\n**Tracking issue:** [TanStack/router#7383](https://github.com/TanStack/router/issues/7383)\n**GitHub Security Advisory:** [GHSA-g7cv-rxg3-hmpx](https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx)\n\n42 packages, 84 versions (two per package, published roughly 6 minutes apart). See the tracking issue for the full table. Confirmed-clean families: @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store, @tanstack/start (the meta-package, not @tanstack/start-*).\n\nWhen a developer or CI environment runs npm install, pnpm install, or yarn install against any affected version, npm resolves the malicious optionalDependencies entry, fetches the orphan payload commit from the fork network, runs its prepare lifecycle script, and executes a ~2.3 MB obfuscated router_init.js smuggled into the affected tarball. The script:\n\nBecause the payload runs as part of npm install's lifecycle, anyone who installed an affected version on 2026-05-11 must treat the install host as potentially compromised.\n\nAll times UTC. Local timestamps from GitHub API and npm registry.\n\n| Time | Event |\n|---|---|\n| 2026-05-10 17:16 | Attacker creates fork\n|\n\n| Time | Event |\n|---|---|\n| 2026-05-11 19:15:44 | Workflow run 25613093674 (Release) is re-run as attempt #4 against main HEAD b1c061af. Original trigger was PR #7369 (Shkumbin's CSS.supports fix), merged 2026-05-09 22:11:51 UTC. Poisoned cache is restored on the runner. |\n| 19:16:18 | Manuel merges PR #7382 (jiti tsconfig paths fix) → push to main triggers a fresh release.yml run |\n| 19:16:22 | Workflow run 25691781302 starts (attempt #1). Same poisoned cache restored. |\n| 19:20:39 | npm registry receives publish for @tanstack/history@1.161.9 and 41 sibling packages from run 25613093674 (~half of the eventual 84 versions; the remainder come during run #2). Publish is authenticated via OIDC trusted-publisher binding for TanStack/router release.yml@refs/heads/main — but it does not come from the workflow's defined Publish Packages step, which was skipped because tests failed. It comes from the malware running during the test/cleanup phase, which mints an OIDC token via the workflow's id-token: write permission and POSTs directly to registry.npmjs.org |\n| 19:20:48 | Run 25613093674 completes (status: failure) |\n| 19:26:14 | npm registry receives publish for the second-version-per-package set (@tanstack/history@1.161.12 etc.) from run 25691781302. Same OIDC mechanism |\n| 19:26:22 | Run 25691781302 completes (status: failure) |\n\n| Time | Event |\n|---|---|\n| 2026-05-11 19:46 | External researcher ashishkurmi working for StepSecurity opens issue #7383 with a complete writeup of the malicious optionalDependencies fingerprint and the package list (initially 14 of the 42) |\n| ~19:50 | Researcher notifies npm security directly |\n| ~20:00 | Manuel acknowledges in #7383 — incident response begins |\n| ~20:10 | Manuel removes all other team push permissions on GitHub in case of user machines have been compromised |\n| 20:19 | Tanner deprecates @tanstack/history@1.161.9 and @1.161.12 — the first two versions taken out of circulation |\n| 20:41 | Batch deprecation runs across the initial 14-package / 28-version scope from issue #7383 |\n| ~21:00 | Comprehensive scan of all 295 @tanstack/* packages confirms full scope: 42 packages, 84 versions (28 more than the StepSecurity report). Public Twitter/X/LinkedIn/Bluesky disclosure from @tan_stack and maintainers |\n| 21:03 | Final batch deprecation runs across the remaining versions to cover the full 42-package / 84-version scope |\n| 21:30 | Investigation identifies bundle-size.yml pull_request_target cache-poisoning vector and the zblgg/configuration fork. All cache entries for all TanStack/* GitHub repositories purged via API. Hardening PR merged: bundle-size.yml restructured, repository_owner guards added, third-party action refs pinned to SHAs. Official GitHub Security Advisory is published, CVE requested |\n| 22:13–23:55 | npm removes the affected tarballs registry-side in response to the StepSecurity notification from earlier in the evening — first removal at 22:13:38 (@tanstack/query-core), last at 23:55:26 (@tanstack/router-core) |\n| 2026-05-12 05:02 | Tanner emails\nFormal malware reports are submitted via npm |\n\nElapsed time from two reference points: the first malicious **publish** (19:20 UTC) and the moment the incident became **known** (StepSecurity issue #7383, 19:46 UTC).\n\n| Milestone | From publish | From known |\n|---|---|---|\n| Compromise publicly detected | ~26 min | — |\n| First versions deprecated (2 of 84) | ~59 min | ~33 min |\n| Initial scope deprecated (28 of 84) | ~1h 21m | ~55 min |\n| Full scope deprecated (84 of 84) | ~1h 43m | ~1h 17m |\n| First package removed by npm | ~2h 53m | ~2h 27m |\n| Last package removed by npm | ~4h 35m | ~4h 9m |\n\nThree vulnerabilities chained together. Each is necessary for the attack; none alone is sufficient.\n\nbundle-size.yml ran pull_request_target for fork PRs and, inside that trigger context, checked out the fork's PR-merge ref and ran a build:\n\n```\non:\n  pull_request_target:\n    paths: ['packages/**', 'benchmarks/**']\n\njobs:\n  benchmark-pr:\n    steps:\n      - uses: actions/checkout@v6.0.2\n        with:\n          ref: refs/pull/${{ github.event.pull_request.number }}/merge # fork's merged code\n\n      - uses: TanStack/config/.github/setup@main # transitively calls actions/cache@v5\n\n      - run: pnpm nx run @benchmarks/bundle-size:build # executes fork-controlled code\non:\n  pull_request_target:\n    paths: ['packages/**', 'benchmarks/**']\n\njobs:\n  benchmark-pr:\n    steps:\n      - uses: actions/checkout@v6.0.2\n        with:\n          ref: refs/pull/${{ github.event.pull_request.number }}/merge # fork's merged code\n\n      - uses: TanStack/config/.github/setup@main # transitively calls actions/cache@v5\n\n      - run: pnpm nx run @benchmarks/bundle-size:build # executes fork-controlled code\n```\n\nThe author of the workflow attempted a trust split (the comment-pr job is separate from benchmark-pr, with a comment in the YAML noting the intent to keep benchmark-pr \"untrusted with read-only permissions\"). The split is correct in spirit but missed two facts:\n\nThe malicious vite_setup.mjs was specifically designed to write data into the pnpm-store directory under a key the legit release.yml workflow would compute and look up: Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')}. When the benchmark-pr job ended, actions/cache@v5's post-step saved the (now-poisoned) pnpm store to that exact key. When release.yml next ran on a push to main, its Setup Tools step restored the poisoned entry — entirely as designed.\n\nThis is the class of attack documented by Adnan Khan in 2024. It's not a TanStack-specific bug; it's a known GitHub Actions design issue that requires conscious mitigation.\n\nrelease.yml declares id-token: write (legitimately needed for npm OIDC trusted publishing). When the poisoned pnpm store is restored on the runner, attacker-controlled binaries are now on disk and get invoked during the build step. Those binaries:\n\nThis is the same memory-extraction technique (and verbatim Python script, with attribution comment) used in the tj-actions/changed-files compromise of March 2025. The attacker did not invent novel tradecraft; they recombined published research.\n\nThe chain only works because each vulnerability bridges the trust boundary the others assumed: PR fork code crossing into base-repo cache, base-repo cache crossing into release-workflow runtime, and release-workflow runtime crossing into npm registry write access.\n\nDetection was external. External researcher ashishkurmi working for StepSecurity opened issue #7383 ~20 minutes after the publish, with full technical analysis. Tanner received a phone call from Socket.dev just moments after starting the war room confirming the situation.\n\nIn any @tanstack/* package's manifest:\n\n```\n\"optionalDependencies\": {\n  \"@tanstack/setup\": \"github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c\"\n}\n\"optionalDependencies\": {\n  \"@tanstack/setup\": \"github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c\"\n}\n```\n\nThese need answers before we close the postmortem.\n\nSee the GitHub Security Advisory for the full list of affected versions: [GHSA-g7cv-rxg3-hmpx](https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx)", "url": "https://wpnews.pro/news/postmortem-tanstack-npm-supply-chain-compromise", "canonical_source": "https://tanstack.com/blog/npm-supply-chain-compromise-postmortem", "published_at": "2026-05-11 12:00:00+00:00", "updated_at": "2026-05-27 09:27:54.145647+00:00", "lang": "en", "topics": ["ai-safety"], "entities": ["Tanner Linsley", "TanStack", "npm", "GitHub Actions", "Router", "Start", "ashishkurmi", "stepsecurity"], "alternates": {"html": "https://wpnews.pro/news/postmortem-tanstack-npm-supply-chain-compromise", "markdown": "https://wpnews.pro/news/postmortem-tanstack-npm-supply-chain-compromise.md", "text": "https://wpnews.pro/news/postmortem-tanstack-npm-supply-chain-compromise.txt", "jsonld": "https://wpnews.pro/news/postmortem-tanstack-npm-supply-chain-compromise.jsonld"}}