Executive Summary
On May 11, 2026, attackers published 84 malicious versions across 42 @tanstack/* npm packages in a six-minute window by chaining three separate GitHub Actions vulnerabilities: a pull_request_target Pwn Request, cross-boundary cache poisoning, and OIDC token extraction from runner memory. The attack — attributed to the cybercriminal group TeamPCP and named "Mini Shai-Hulud" by Snyk researchers — produced the first known npm supply-chain compromise bearing valid SLSA Build Level 3 attestations, meaning standard provenance checks would have passed. Any developer who ran npm install, pnpm install, or yarn install against an affected package version on that date should rotate every credential reachable from the install host immediately.
1. What Is This Vulnerability?
CVE-2026-45321 is a supply-chain attack that exploited three chained GitHub Actions weaknesses to publish credential-stealing malware under the trusted identity of the TanStack project. None of the three weaknesses alone was sufficient — the attack only succeeded because each one bridged a trust boundary the others assumed.
Attack Vector
Step 1 — The Pwn Request (cache poisoning phase)
The attacker created a fork of TanStack/router, deliberately renamed to zblgg/configuration to evade fork-list searches. They authored a malicious commit using the fabricated identity claude <claude@users.noreply.github.com> (no relation to Anthropic's AI) and embedded a ~30,000-line obfuscated JS payload inside packages/history/vite_setup.mjs. The commit used [skip ci] in the message to suppress CI on push.
The attacker then opened PR #7378 against TanStack/router#main. Because the repo contained a bundle-size.yml workflow using the pull_request_target trigger — which runs in the base repo's privileged context, not the fork's — the workflow automatically ran for this external PR without requiring maintainer approval. The critical flaw was that bundle-size.yml checked out the PR's merge ref (fork-controlled code) and executed it:
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 code runs here
- uses: TanStack/config/.github/setup@main # transitively calls actions/cache@v5
- run: pnpm nx run @benchmarks/bundle-size:build # ← executes attacker's vite_setup.mjs
Step 2 — Cache Poisoning across the fork↔base trust boundary
When the benchmark job ran, actions/cache@v5's post-job hook saved the (now-poisoned) pnpm store to the cache key Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')}. This cache entry is scoped to the base repository — not the fork — meaning it persists and is accessible to production workflows running on main. The attacker calculated the exact cache key the legitimate release.yml workflow would look up and wrote a poisoned pnpm store to that key.
After the cache was poisoned, the attacker force-pushed the PR back to the current main HEAD (a 0-file, no-op diff), then closed the PR and deleted the branch. The poisoned cache entry remained, invisible.
Step 3 — OIDC Token Extraction from Runner Memory
release.yml legitimately declares id-token: write to enable npm's OIDC trusted publishing. When a subsequent push to main triggered release.yml, it restored the poisoned cache. Attacker-controlled binaries now on disk were invoked during the build phase. Those binaries:
- Located the
GitHub.Actions.Runner.Workerprocess via/proc/*/cmdline - Read
/proc/<pid>/mapsand/proc/<pid>/memto dump the runner's memory - Extracted the OIDC token the runner mints lazily in memory when
id-token: writeis set - Used that token to POST directly to
registry.npmjs.org— bypassing the workflow's officialPublish Packagesstep entirely
The malicious publish was authenticated with the exact same OIDC trusted-publisher binding TanStack legitimately uses, producing packages with valid SLSA Build Level 3 provenance attestations.
Step 4 — The Malware Payload
When any developer ran install against an affected version, npm resolved a malicious optionalDependencies entry, fetched an orphan payload commit from the fork network, and executed router_init.js (~2.3 MB, obfuscated) via npm's prepare lifecycle hook. The script:
- Harvested credentials from: AWS IMDS/Secrets Manager, GCP metadata, Kubernetes service-account tokens, Vault tokens,
~/.npmrc, GitHub tokens (env, gh CLI,.git-credentials), and SSH private keys - Exfiltrated over the Session/Oxen encrypted messenger network (
filev2.getsession.org,seed{1,2,3}.getsession.org) — no attacker-controlled C2, making IP/domain blocking the only network-level mitigation - Self-propagated: enumerated other packages the victim maintains via the npm registry API and republished them with the same injection — creating a worm that spread to
@squawk/*,@mistralai/*, and other ecosystems
Real-World Impact
84 malicious package versions across 42 packages were live on npm for between 2 hours 53 minutes and 4 hours 35 minutes. The attack was detected externally by StepSecurity researcher ashishkurmi approximately 20 minutes after the initial publish. Because the malware ran at npm install time, any machine that installed an affected version must be treated as fully compromised. The self-propagating worm extended the blast radius beyond TanStack to other maintained packages in the npm ecosystem.
CISA added CVE-2026-45321 to its Known Exploited Vulnerabilities (KEV) catalog on May 27, 2026, with a remediation deadline of June 10, 2026 for federal agencies.
2. Who Is Affected?
Directly compromised packages — 42 packages from the TanStack/router monorepo (Router and Start packages only):
@tanstack/router,@tanstack/router-core,@tanstack/router-cli@tanstack/start-*(all sub-packages)@tanstack/history- All other packages in the TanStack/router monorepo
Confirmed safe — All other TanStack families are unaffected: @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store, @tanstack/devtools*.
At risk if you:
- Ran
npm install,pnpm install, oryarn installon May 11, 2026 (any time zone) - Have
@tanstack/routeror@tanstack/start-*packages and ran install that day - Used a CI/CD pipeline that resolved fresh dependencies on May 11, 2026
- Installed from a lockfile that pinned to the affected version range (see Affected Versions in the GitHub Security Advisory GHSA-g7cv-rxg3-hmpx)
Secondary exposure — Any package maintained by someone who ran npm install against an affected version (via worm self-propagation).
3. How to Detect It (Testing)
Manual Testing Steps
Step 1: Check your npm install logs for the affected date
# Check npm cache entries from May 11, 2026
ls -la ~/.npm/_cacache/ | grep "2026-05-11"
# Check pnpm cache
ls -la ~/.local/share/pnpm/store/v3/
Step 2: Scan installed packages for the malicious optionalDependency fingerprint
# In any project directory, scan all installed package.json files
find node_modules -name "package.json" -not -path "*/node_modules/*/node_modules/*" | \
xargs grep -l '"@tanstack/setup"' 2>/dev/null
# Look for the malicious GitHub commit reference specifically
find node_modules -name "package.json" | \
xargs grep -l "79ac49eedf774dd4b0cfa308722bc463cfe5885c" 2>/dev/null
Step 3: Check for the malicious router_init.js payload file
find node_modules -name "router_init.js" -size +2M 2>/dev/null
# Check for the specific payload in any @tanstack package
find node_modules/@tanstack -name "router_init.js" 2>/dev/null
Step 4: Check network connections for exfiltration indicators
# Check for connections to Session/Oxen exfiltration network
grep -r "getsession.org\|filev2.getsession\|seed1.getsession\|seed2.getsession\|seed3.getsession" \
~/.npm/ node_modules/ 2>/dev/null
# Check DNS resolution history (macOS)
log show --predicate 'process == "mDNSResponder"' --last 30d 2>/dev/null | \
grep "getsession.org"
Step 5: Verify installed package versions against safe versions
# List all @tanstack package versions installed
npm ls | grep "@tanstack"
# or
cat package-lock.json | python3 -c "
import sys, json
lock = json.load(sys.stdin)
tanstack = {k: v['version'] for k, v in lock.get('packages', {}).items() if '@tanstack' in k}
for k, v in tanstack.items():
print(f'{k}: {v}')
"
Step 6: Cross-reference against the known malicious IOC
Malicious optionalDependencies fingerprint:
"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
Malicious file: router_init.js (~2.3 MB, not in "files" field of package.json)
Malicious cache key: Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11
2nd-stage payload URLs: https://litter.catbox.moe/h8nc9u.js, https://litter.catbox.moe/7rrc6l.mjs
Exfiltration endpoints: filev2.getsession.org, seed{1,2,3}.getsession.org
Automated Scanning
Tool: Socket.dev Socket monitors npm package manifests for malicious optionalDependencies and behavioral anomalies. Run a scan of your dependency tree:
npm install -g @socketsecurity/cli
socket scan .
Expected output: Flags the @tanstack/setup github dependency pattern and the router_init.js file anomaly.
Tool: Snyk
npm install -g snyk
snyk test --all-projects
Expected output: CVE-2026-45321 flagged on any affected @tanstack/router version.
Tool: Grype (Anchore)
grype dir:node_modules --fail-on critical
Expected output: Reports GHSA-g7cv-rxg3-hmpx if affected packages are present.
Tool: StepSecurity Harden-Runner For CI/CD pipeline scanning, install StepSecurity's Harden-Runner in GitHub Actions workflows. It was the tool that detected the original attack within 20 minutes:
- uses: step-security/harden-runner@v3
with:
egress-policy: audit
allowed-endpoints: >
registry.npmjs.org:443
# ... your allowed endpoints
Code Review Checklist for Your GitHub Actions Workflows
- Search all workflows for
pull_request_targettrigger:grep -r "pull_request_target" .github/workflows/ - Verify no
pull_request_targetworkflow checks out fork code withref: refs/pull/*/merge - Confirm
actions/cacheis not used inpull_request_targetjobs that execute untrusted code - Check that
id-token: writepermission is scoped only to the specific job that needs it, not the entire workflow - Verify all third-party Actions are pinned to full SHA hashes, not floating tags like
@v3or@main - Run:
grep -r "id-token: write" .github/workflows/and audit every match
4. How to Fix It (Mitigation)
Step-by-Step Remediation
If you installed affected packages on May 11, 2026:
-
Treat the install host as fully compromised. This is not optional — the malware ran at install time.
-
Immediately rotate all credentials reachable from the affected machine:
- AWS: Rotate IAM access keys, invalidate any temporary credentials from IMDS
- GCP: Rotate service account keys, revoke OAuth tokens
- Kubernetes: Rotate service-account tokens, check RBAC for damage
- Vault: Revoke all tokens from the affected host
- GitHub: Rotate personal access tokens, OAuth tokens, and SSH keys
- npm: Revoke npm auth tokens (
npm token revoke <token>) - SSH: Generate new key pairs; remove the old public key from all authorized_keys
-
Revoke and re-issue any code-signing certificates accessible from the machine.
-
Update to safe package versions immediately:
# Update all @tanstack/router family packages npm install @tanstack/router@latest @tanstack/start@latest # or with pnpm pnpm update "@tanstack/*"All currently published versions of TanStack Router/Start are clean (TanStack issued an all-clear on May 15, 2026).
-
Audit your own npm packages if you are a maintainer. Check whether the worm published infected versions of packages you maintain:
npm view <your-package-name> versions --json | tail -20Compare publish dates; any publish on May 11–12, 2026 you don't recognize should be treated as a worm propagation.
-
Clear all npm, pnpm, and yarn caches on affected machines:
npm cache clean --force pnpm store prune yarn cache clean -
Clear GitHub Actions cache for any repositories that ran workflows on May 11, 2026 with
@tanstack/routerdependencies:gh cache list --repo <your-org>/<your-repo> gh cache delete --all --repo <your-org>/<your-repo>
Hardening Your GitHub Actions Workflows (Prevention)
Fix 1: Never execute fork code in pull_request_target context
# VULNERABLE — do not do this
on:
pull_request_target:
jobs:
build:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # ← fork code in privileged context
# SAFE — split into two workflows
# Workflow 1: runs on pull_request (unprivileged)
on:
pull_request:
jobs:
build:
steps:
- uses: actions/checkout@v4 # checks out fork code, no privilege
- run: pnpm build
- uses: actions/upload-artifact@v4
with:
name: build-results-${{ github.event.number }}
path: ./dist
# Workflow 2: runs on workflow_run (privileged, but only reads artifact)
on:
workflow_run:
workflows: ["Build PR"]
types: [completed]
jobs:
post-results:
if: github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/download-artifact@v4
with:
name: build-results-${{ github.event.workflow_run.id }}
# Comment with results, no fork code executed here
Fix 2: Scope id-token: write to the specific publish job only
# VULNERABLE — entire workflow has id-token: write
permissions:
id-token: write
contents: read
# SAFE — only the publish job gets the permission
jobs:
test:
permissions:
contents: read # no id-token here
steps: [...]
publish:
needs: test
permissions:
id-token: write # only here, only when needed
contents: read
steps:
- uses: actions/setup-node@v4
with:
registry-url: 'https://registry.npmjs.org'
- run: npm publish --provenance
Fix 3: Pin all Actions to full SHA hashes
# VULNERABLE — floating tags
- uses: actions/checkout@v6.0.2
- uses: actions/cache@v5
# SAFE — pinned to commit SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v6.0.2
- uses: actions/cache@5a3ec84eff668545d3b9f69bf1de44a5b7e1aa6d # v5.0.2
Fix 4: Add repository_owner guard to all pull_request_target workflows
jobs:
benchmark-pr:
# Only run if the PR comes from within the same org/owner
if: github.event.pull_request.head.repo.owner.login == github.repository_owner
steps: [...]
Configuration Hardening
Enable these npm security settings in your .npmrc:
# Disable lifecycle scripts from untrusted packages (breaks some packages — test first)
ignore-scripts=false # keep this false for most uses, but audit what scripts run
# Require package lock to prevent resolution drift
package-lock=true
# Enable audit on every install
audit=true
For organizations: enforce OIDC trusted publishing with npm's publish provenance and monitor publish events via npm's org audit log.
5. How to Test the Fix (Validation)
Regression Test Scenarios
- Scenario A: Verify the malicious
optionalDependenciesfingerprint no longer exists in any installed package - Scenario B: Confirm
npm auditreturns zero critical findings for CVE-2026-45321 - Scenario C: Verify all
@tanstack/*packages installed resolve to post-incident clean versions - Scenario D: Confirm no
router_init.jsfile exists innode_modules/@tanstack/
Security Test Cases
Test Case 1: Verify no malicious packages installed
- Precondition: Update to latest
@tanstack/*versions and runnpm install - Steps:
find node_modules/@tanstack -name "router_init.js" 2>/dev/null grep -r "getsession.org" node_modules/ 2>/dev/null grep -r "79ac49eedf774dd4b0cfa308722bc463cfe5885c" node_modules/ 2>/dev/null - Expected Result: No output — all three commands return nothing
Test Case 2: Verify npm audit is clean
- Precondition: Updated
package.jsonwith latest@tanstack/* - Steps:
npm audit --audit-level=moderate - Expected Result:
found 0 vulnerabilitiesor no mention of CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx
Test Case 3: Verify GitHub Actions workflows are hardened
- Precondition: Applied workflow fixes above
- Steps:
# Check for any remaining pull_request_target with checkout of fork refs grep -A 20 "pull_request_target" .github/workflows/*.yml | grep "refs/pull" # Check for floating Action refs grep -r "uses:.*@v[0-9]" .github/workflows/ | grep -v "#.*sha" - Expected Result: No output from either command
Test Case 4: Verify OIDC scope is restricted
- Precondition: Updated workflows with job-level permission scoping
- Steps: Review each workflow file and confirm
id-token: writeappears only under specificjobs.<job-id>.permissions, not at the top-levelpermissions:block - Expected Result:
grep "id-token: write" .github/workflows/*.ymlshows it only inside specific job blocks
Automated Tests
Add this to your CI pipeline as a supply-chain integrity gate:
#!/bin/bash
# supply-chain-check.sh — Run after npm install in CI
set -e
echo "=== Supply Chain Integrity Check ==="
# Check for known malicious IOC fingerprints
MALICIOUS_COMMIT="79ac49eedf774dd4b0cfa308722bc463cfe5885c"
EXFIL_DOMAIN="getsession.org"
MALICIOUS_FILE="router_init.js"
echo "Checking for malicious commit reference..."
if find node_modules -name "package.json" | xargs grep -l "$MALICIOUS_COMMIT" 2>/dev/null | grep -q .; then
echo "FAIL: Malicious commit reference found in node_modules"
exit 1
fi
echo "Checking for exfiltration domain references..."
if grep -r "$EXFIL_DOMAIN" node_modules/ 2>/dev/null | grep -q .; then
echo "FAIL: Exfiltration domain reference found in node_modules"
exit 1
fi
echo "Checking for unexpected large JS files in @tanstack packages..."
LARGE_FILES=$(find node_modules/@tanstack -name "*.js" -size +2M 2>/dev/null)
if [ -n "$LARGE_FILES" ]; then
echo "WARN: Large JS files found in @tanstack: $LARGE_FILES"
exit 1
fi
echo "=== All checks passed ==="
6. Prevention & Hardening
Best Practices
Practice 1: Audit all pull_request_target workflows immediately
This is the most important action. Search every repository for this dangerous pattern and apply the fix described in Section 4. GitHub's Security Lab has documented this class of attack since 2021; any workflow still using the vulnerable pattern is a standing supply-chain risk.
Practice 2: Implement dependency pinning with lockfile integrity checks
Always commit package-lock.json or pnpm-lock.yaml. In CI, use npm ci (not npm install) to enforce lockfile-exact installs. Consider using a tool like Renovate with automerge for dependency updates rather than allowing floating version ranges.
Practice 3: Enable npm publish provenance AND verify it SLSA attestations are only useful if you verify them. CVE-2026-45321 produced valid SLSA Level 3 attestations for malicious packages — because the malware minted a legitimate OIDC token. Provenance tells you where a package was built, not whether the build environment was clean. Pair provenance with behavioral analysis tools like Socket.dev or Snyk's real-time monitoring.
Practice 4: Restrict npm publish permissions with 2FA and granular tokens
# Create a publish-only, automation-scoped npm token (not a classic full-access token)
npm token create --cidr-whitelist=<your-ci-ip> --read-only=false
Enable 2FA enforcement on the npm organization: npm org set 2fa-enforcement <org> enforced
Practice 5: Use StepSecurity's Harden-Runner or equivalent egress monitoring in CI
The original attack was detected by a StepSecurity researcher — the tool they use is publicly available. Adding egress monitoring to your GitHub Actions workflows would have caught the exfiltration to getsession.org immediately.
Monitoring & Detection
Set up alerts for anomalous npm publish activity from your organization:
# Monitor your npm org's publish audit log (requires org admin)
npm audit --json | jq '.advisories | to_entries[] | select(.value.severity == "critical")'
# GitHub: Watch for unexpected workflow runs on protected branches
gh api repos/{owner}/{repo}/actions/runs --jq \
'.workflow_runs[] | select(.conclusion == "failure") | {id, name, created_at}'
DNS/Network monitoring rules to add:
# Block/alert on connections to known Mini Shai-Hulud exfiltration endpoints
ALERT: dns query matches *.getsession.org
ALERT: dns query matches filev2.getsession.org
ALERT: http connection to litter.catbox.moe from build agent
GitHub Advanced Security alert rules:
If you use GitHub Advanced Security (GHAS), enable secret scanning push protection and code scanning with the pull_request_target misuse query from CodeQL's Actions security suite.
References
- CVE Record: CVE-2026-45321 on CVE.org
- GitHub Security Advisory: GHSA-g7cv-rxg3-hmpx
- TanStack Official Postmortem: Postmortem: TanStack npm supply-chain compromise
- TanStack Hardening Follow-up: Hardening TanStack After the npm Compromise
- Snyk Analysis: TanStack npm Packages Hit by Mini Shai-Hulud
- CISA KEV Catalog Entry: CISA Adds Three Known Exploited Vulnerabilities – May 27, 2026
- Security Affairs Coverage: U.S. CISA adds Daemon Tools, TanStack, and Nx Console flaws to KEV
- Background — Cache Poisoning Research: The Monsters in Your Build Cache: GitHub Actions Cache Poisoning (Adnan Khan, 2024)
- Background — Pwn Requests: GitHub Security Lab: Keeping your GitHub Actions and workflows secure
- Tracking Issue: TanStack/router#7383