ArbindBuilds LogoArbindBuilds
Blog
CheatsheetsProjectsLinksAbout
Hire Me

ArbindBuilds

Build. Design. Repeat.

© 2026 ArbindBuilds.
All rights reserved.

Site Map

  • Home
  • Blog
  • Projects
  • About
  • Uses

Content

  • Cheatsheets
  • AI Tools
  • AI Prompts
  • Links

Products

  • Speakify
  • Gumroad Store
  • GitHub
  • Twitter / X

Made with care in Assam, India.

  1. Home/
  2. Blog/
  3. How 84 Malicious TanStack Packages Hit npm in 6 Minutes
development
Arbind Singh·May 13, 2026·5 min read·

How 84 Malicious TanStack Packages Hit npm in 6 Minutes

On May 11, 2026, an attacker published 84 malicious versions across 42 @tanstack/* packages in under 6 minutes. Not a typo. Here is the exact chain that made it possible. 42 @tanstack packages compromised via GitHub Actions cache poisoning and OIDC token extraction

How 84 Malicious TanStack Packages Hit npm in 6 Minutes

This is not a story about a zero-day. Every single technique used here was documented publicly before the attack. The attacker recombined existing research, chained three known vulnerabilities, and walked right through the door that was already open.

The Three-Part Chain

No single vulnerability made this possible. Each one bridges a trust boundary the next one needs.

1. pull_request_target Pwn Request

bundle-size.yml used the pull_request_target event trigger. This trigger runs in the context of the base repository, not the fork. It has access to secrets, cache, and write permissions that pull_request does not.

The workflow checked out the fork's PR code and ran a build against it:

on:
  pull_request_target:
    paths: ['packages/**', 'benchmarks/**']

jobs:
  benchmark-pr:
    steps:
      - uses: actions/[email protected]
        with:
          ref: refs/pull/${{ github.event.pull_request.number }}/merge

      - uses: TanStack/config/.github/setup@main

      - run: pnpm nx run @benchmarks/bundle-size:build

That last run step executes arbitrary fork-controlled code with base-repo permissions. The author knew this was dangerous — there's a comment in the YAML noting the intent to keep benchmark-pr "untrusted with read-only permissions." The intent was right. The implementation missed something critical.

2. GitHub Actions Cache Poisoning

Setting permissions: contents: read does not prevent cache writes. actions/cache@v5 saves cache using a runner-internal token, not the workflow GITHUB_TOKEN. So the "read-only" workflow could still write to the shared pnpm cache.

The attacker's malicious vite_setup.mjs (smuggled into the PR as packages/history/vite_setup.mjs, ~30,000 lines of bundled JS) ran during the benchmark build and wrote a poisoned pnpm store to cache key:

Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11

That key is computed from hashFiles('**/pnpm-lock.yaml'). The attacker crafted the cache entry to match exactly what release.yml would look up on the next push to main. Cache scope in GitHub Actions is per-repo, shared between pull_request_target runs and pushes. The PR running on fork code wrote to the same cache namespace that production workflows use.

Cache poisoning as a class was documented by Adnan Khan in May 2024. This is not a new attack surface.

3. OIDC Token Extraction from Runner Memory

release.yml declared id-token: write for npm's OIDC trusted-publisher binding. When the poisoned pnpm store was restored on the runner, attacker-controlled binaries were now on disk. They located the GitHub Actions Runner.Worker process, read /proc/<pid>/maps and /proc/<pid>/mem to dump worker memory, and extracted the OIDC token the runner had minted in memory.

That token was used to POST directly to registry.npmjs.org — bypassing the workflow's actual Publish Packages step entirely. The publish step never ran because tests failed. The malware did not need it.

The Python script used for memory extraction was verbatim from the tj-actions/changed-files compromise in March 2025, attribution comment and all.

What the Malware Actually Did

When a developer or CI environment ran npm install against any affected version, the package's prepare lifecycle script executed router_init.js (~2.3 MB, obfuscated, not in the package's files field).

That script:

  • Harvested credentials from AWS IMDS / Secrets Manager, GCP metadata, Kubernetes service-account tokens, Vault tokens, ~/.npmrc, GitHub tokens, SSH private keys
  • Exfiltrated over the Session/Oxen messenger file-upload network (filev2.getsession.org, seed{1,2,3}.getsession.org) — end-to-end encrypted, so no attacker-controlled C2 server to block by reputation
  • Self-propagated by enumerating packages the victim maintains on npm and republishing them with the same payload

If you ran npm install on an affected @tanstack/* package on May 11, 2026, treat the install host as compromised. Rotate AWS, GCP, Kubernetes, Vault, GitHub, npm, and SSH credentials reachable from it.

IOC Fingerprints

In any affected package.json:

"optionalDependencies": {
  "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}
  • File: router_init.js (~2.3 MB, package root)
  • Cache key: Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11
  • Exfiltration endpoints: filev2.getsession.org, seed{1,2,3}.getsession.org
  • Payload commit: 79ac49eedf774dd4b0cfa308722bc463cfe5885c (orphan commit in fork network)
  • Attacker fork: github.com/zblgg/configuration (renamed from TanStack/router to evade fork-list searches)
  • Forged commit identity: claude <[email protected]> — not the real Anthropic Claude, a fabricated no-reply email

What TanStack Missed

Detection came from an external researcher at StepSecurity, 20 minutes after publish. Not from TanStack's own tooling. There was no internal alerting on npm publishes. For a project with 42 packages and 7 npm maintainers, that gap is worth thinking about.

A few other things that made this worse than it had to be:

npm unpublish is effectively unavailable at scale. Once packages have dependents, npm won't let you unpublish. The only option is deprecation and waiting for npm security to pull tarballs server-side. That added hours of exposure.

OIDC trusted-publisher has no per-step scope. Once id-token: write is on the workflow, any code running during that workflow can mint a publish-capable token. There is no "only the Publish Packages step can use this." The permission is workflow-wide.

Floating action refs are a standing risk. @v6.0.2 and @main on third-party actions mean an upstream compromise or a force-push can change what runs in your CI without a PR to your repo.

What to Actually Fix

TanStack published a hardening post the next day. The changes worth noting:

  • bundle-size.yml restructured so fork code never runs in pull_request_target context
  • repository_owner guards added to workflows
  • Third-party action refs pinned to SHAs
  • All GitHub Actions caches purged across all TanStack repos

If you run a monorepo with similar CI patterns, audit every pull_request_target workflow you have. If any of them check out fork code and run a build, you have the same vulnerability.

The attack chain here used entirely public tradecraft, chained methodically, against a popular open-source project that many production apps depend on. Nobody invented anything new. The attacker just read the research and executed it.

That should bother you more than a zero-day would.


References

  • TanStack postmortem
  • GitHub Security Advisory GHSA-g7cv-rxg3-hmpx
  • Adnan Khan — GitHub Actions cache poisoning (2024)
  • GitHub Security Lab — preventing Pwn Requests
Arbind Singh

Arbind Singh

ArbindBuilds is my digital space where I showcase my projects, share insightful blogs, and document my work and ideas.

Comments

Leave a comment

0/500 characters

READ NEXT

Lovable Leaks Source Code: The $6.6B BOLA Vulnerability

An 8 million user platform ignored a critical BOLA vulnerability for 48 days. How a $6.6B AI app builder leaked source code, credentials, and user data.

Read →

Kubernetes vs Docker: Stop Comparing the Wrong Things

Docker builds containers. Kubernetes runs them at scale. They're not rivals and picking the wrong mental model for each costs you months of overhead.

Read →

Building a Personal MCP Server for Claude

Stop copy-pasting your bio. Build a custom Model Context Protocol (MCP) server in TypeScript to give Claude native access to your projects, resume, and personal context.

Read →

Tagged

tanstacknpmsupply-chaingithub-actionssecurityoidcci-cdopen-sourcedeveloperjavascripttypescriptsaas
← Back to Blog