The Attack in a Nutshell

On May 11, 2026, between 19:20 and 19:26 UTC, an attacker published 84 malicious versions across 42 @tanstack/* npm packages. The attack combined three known GitHub Actions vulnerabilities: the pull_request_target "Pwn Request" pattern, 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.

Timeline of Events

Pre-attack: Cache Poisoning Phase (May 10–11)

  1. Fork creation: Attacker creates fork github.com/zblgg/configuration (a fork of TanStack/router, renamed to evade fork-list searches).
  2. Malicious commit: A commit with message [skip ci] adds packages/history/vite_setup.mjs (~30,000 lines of bundled JS) under a fabricated identity claude .
  3. PR opened: PR #7378 titled "WIP: simplify history build" triggers pull_request_target workflows (bundle-size.yml and labeler.yml). The pr.yml workflow (which uses pull_request) does not run, blocked pending approval.
  4. Cache poison: The bundle-size.yml workflow checks out the PR merge ref, runs pnpm install and pnpm nx run @benchmarks/bundle-size:build, executing vite_setup.mjs. This script writes malicious data into the pnpm store directory. The actions/cache@v5 post-step saves the poisoned cache under key Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11 — a key that the legitimate release.yml workflow would compute on the next push to main.
  5. Cleanup: Attacker force-pushes the PR back to current main HEAD, making it a 0-file no-op, then closes PR and deletes branch. Cache poison persists.

Detonation: Publish Phase (May 11, 19:15–19:26)

  1. Trigger: Manuel merges two PRs (#7369 and #7382) to main, each triggering release.yml.
  2. Cache restore: Both workflow runs restore the poisoned cache. The malicious binaries are now on disk.
  3. OIDC token theft: The malware locates the GitHub Actions Runner.Worker process via /proc/*/cmdline, reads /proc//maps and /proc//mem to dump memory, and extracts the OIDC token (which the runner mints lazily when id-token: write is set). This technique was previously used in the tj-actions/changed-files compromise of March 2025.
  4. Malicious publish: The malware uses the stolen OIDC token to POST directly to registry.npmjs.org, publishing two malicious versions per package (e.g., @tanstack/history@1.161.9 and @tanstack/history@1.161.12). The legitimate "Publish Packages" step is skipped because tests fail.

What the Malware Does

When a developer runs npm install, pnpm install, or yarn install against an affected version, npm resolves the malicious optionalDependencies entry:

"optionalDependencies": {
  "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}

This fetches an orphan payload commit from the fork network and runs its prepare lifecycle script, which executes a ~2.3 MB obfuscated router_init.js. The script:

  • Harvests credentials from AWS IMDS / Secrets Manager, GCP metadata, Kubernetes service-account tokens, Vault tokens, ~/.npmrc, GitHub tokens (env, gh CLI, .git-credentials), SSH private keys
  • Exfiltrates over the Session/Oxen messenger file-upload network (filev2.getsession.org, seed{1,2,3}.getsession.org) — end-to-end encrypted with no attacker-controlled C2
  • Self-propagates: enumerates other packages the victim maintains via registry.npmjs.org/-/v1/search?text=maintainer: and republishes them with the same injection

Detection and Response

  • 20 minutes: External researcher carlini opens issue #7383 with full analysis. npm security is notified.
  • ~20:00 UTC: Manuel acknowledges the incident, removes all other team push permissions on GitHub.
  • ~21:00 UTC: Comprehensive scan confirms 42 packages, 84 versions affected. Tanner begins npm deprecation. Public disclosure on Twitter/X/LinkedIn/Bluesky.
  • 21:30 UTC: Cache entries purged. Hardening PR merged: bundle-size.yml restructured, repository_owner guards added, third-party action refs pinned to SHAs. Official GitHub Security Advisory published.

Three Vulnerabilities Chained

  1. pull_request_target "Pwn Request" in bundle-size.yml: The workflow checked out the fork's PR-merge ref and ran a build, giving fork code access to the base repo's cache scope. actions/cache@v5's post-job save is not gated by permissions: contents: read.
  2. Cache poisoning across trust boundaries: The poisoned cache was keyed to match what release.yml would look up on push to main. This is a known GitHub Actions design issue (Adnan Khan, 2024).
  3. OIDC token memory extraction: The workflow had id-token: write (needed for npm OIDC trusted publishing). Malicious binaries on disk could dump the runner's memory and extract the token, then use it to publish to npm directly.

IOC Fingerprints

  • Manifest: "optionalDependencies": { "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" }
  • File: router_init.js (~2.3 MB, package root, not in "files")
  • Cache key: Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11
  • 2nd-stage payload URLs: https://litter.catbox.moe/h8nc9u.js, https://litter.catbox.moe/7rrc6l.mjs
  • Exfiltration network: filev2.getsession.org, seed{1,2,3}.getsession.org
  • Forged commit identity: claude
  • Attacker accounts: zblgg (id 127806521), voicproducoes (id 269549300)
  • Attacker fork: github.com/zblgg/configuration (fork of TanStack/router renamed)
  • Orphan payload commit: 79ac49eedf774dd4b0cfa308722bc463cfe5885c

What You Should Do Now

If you installed any affected version on 2026-05-11, rotate AWS, GCP, Kubernetes, Vault, GitHub, npm, and SSH credentials reachable from the install host. Treat the host as potentially compromised. All affected versions have been deprecated; npm security has been engaged to pull tarballs from the registry.

Mitigations for Maintainers

  • Avoid pull_request_target with checkout of fork code. If you must use it, add explicit repository_owner checks and pin action refs to SHAs.
  • Use separate cache scopes for PR vs. push workflows, or disable cache for pull_request_target workflows.
  • For OIDC publishing, consider using a separate workflow with minimal permissions and no cache restore from PR-scoped caches.
  • Monitor for unexpected cache key collisions and consider cache prefixing by branch.