Ten minutes, 84 packages, zero stolen tokens
On May 12, 2026, someone published 84 malicious npm package versions across the TanStack namespace in a six-minute window starting at approximately 19
UTC. The packages spanned 42 libraries with two poisoned versions each. No npm access token was leaked. No typosquat was involved. The attacker walked through the front door using the project’s own OIDC trusted-publisher binding with npm, which is the mechanism the JavaScript ecosystem adopted specifically to prevent exactly this kind of compromise.The three-step Actions abuse chain
Cybersecurity News’ analysis describes the attack as a chain of three GitHub Actions exploits, each one enabling the next.
Step 1: Pwn Request via pull_request_target. The attacker opened a pull request against a TanStack repository. The repo’s CI workflow used pull_request_target, which runs in the context of the base repository and has access to its secrets, including the OIDC trust relationship with npm. The malicious PR’s code executed inside that trusted context.
Step 2: Actions cache poisoning. Using the trust boundary created by the fork-to-base interaction, the attacker poisoned the repository’s Actions cache. This persisted malicious payloads across workflow runs, ensuring that subsequent CI jobs would execute attacker-controlled code even after the original PR was closed.
Step 3: OIDC token extraction from runner memory. The attacker’s code extracted the OIDC token from the GitHub Actions runner process at runtime. With that token in hand, they authenticated to npm as the TanStack project and published 84 package versions that npm’s registry accepted as legitimate, because they were legitimate from the registry’s perspective. The OIDC token was valid, the workflow binding was valid, and the publisher identity matched.
The malicious commit, hash 79ac49eedf774dd4b0cfa308722bc463cfe5885c, registered a prepare lifecycle hook that executed bun run tanstack_runner.js && exit 1. This ran arbitrary code automatically on npm install across both developer workstations and CI runners.
Why OIDC trusted publishing was the enabler
OIDC-based trusted publishing was supposed to eliminate the class of attacks where a leaked npm token leads to package takeover. The idea: instead of storing a long-lived NPM_TOKEN as a GitHub secret, the CI runner requests a short-lived OIDC token from GitHub’s identity provider, and npm verifies that the token’s claims match the repository and workflow that requested it.
That model assumes the workflow itself is uncompromised. The TanStack attack breaks that assumption cleanly. The workflow was the credential, and the attacker obtained it not by stealing a secret but by getting their code to run inside the trusted workflow.
The burden shifts from “protect the token” to “protect the workflow that requests the token.” That is a harder problem, because workflows are code, code has bugs, and the JavaScript ecosystem has thousands of high-traffic packages whose CI configurations are maintained by volunteers.
The blast radius
@tanstack/react-router alone receives over 12 million weekly downloads. The 2.3 MB obfuscated router_init.js payload was not subtle about its targets: it scanned CI environments for AWS credentials, GCP service account keys, Kubernetes configs, HashiCorp Vault tokens, GitHub tokens, SSH keys, and .npmrc contents.
TanStack Query has 49,500 GitHub stars. TanStack Router has 14,500. TanStack Table has 28,000. The ecosystem sits at the center of the JavaScript/TypeScript dependency graph. Any CI runner that installed a poisoned version during the exposure window potentially leaked its credentials to the attacker’s exfiltration endpoint.
What the remediation reveals
TanStack revoked the compromised OIDC bindings and yanked the 84 malicious versions. The response was fast relative to historical npm incidents. But the remediation also revealed the gap: there is no retroactive integrity check in npm’s trusted-publishing flow. The registry accepted the packages because the OIDC claims were valid at publish time. Once published, the packages were distributed through the standard CDN pipeline until they were manually unpublished.
This is the structural weakness. OIDC trust is evaluated at the moment of publish. If the workflow that triggered the publish was compromised, the trust decision was wrong, but the registry has no mechanism to detect that after the fact. The integrity guarantee is forward-only: the registry can verify who published, but not why they published.
Hardening checklist for OIDC-based trusted publishing
Any organization using GitHub Actions with npm trusted publishing should treat the CI workflow as a privileged boundary, not a convenience layer.
- Eliminate
pull_request_targetfrom publish workflows. Usepull_requestfor external contributions, which runs in the fork’s context without access to repository secrets. Ifpull_request_targetis required for a separate job (labeling, CI on the base branch), isolate it in its own workflow file that never has access to OIDC or publish permissions. - Pin action refs to full commit SHAs, not tags. The
actions/checkout@v4tag can be retagged.actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11cannot. - Isolate the publish job. The job that calls
npm publishshould do nothing else. No test execution, no linting, no dependency installation from untrusted sources. Each additional step is attack surface. - Restrict OIDC token claims. Configure the npm trusted-publishing binding to require specific repository, workflow, and environment claims. The tighter the claim set, the smaller the window for an attacker to replay or misuse the token.
- Require manual approval for publish environments. GitHub Environments can require a designated reviewer to approve a deployment before the job runs. Adding this gate to the publish environment means a human signs off before the OIDC token is minted.
- Monitor
prepareandpostinstalllifecycle hooks. The TanStack payload usedprepare, which runs onnpm install. Audit any package that registers lifecycle hooks, and consider using--ignore-scriptsin CI with an explicit allowlist.
The inverted threat model
The TanStack incident does not invalidate OIDC trusted publishing. It does something more useful: it clarifies the threat model. When the ecosystem moved from long-lived npm tokens to OIDC, the question shifted from “who has the token?” to “whose code is running when the token is requested?” That is a better question, but it is not an easy one.
The packages most at risk are not the obscure ones. They are the ones with millions of weekly downloads, maintained by volunteers, whose CI configurations are public and whose pull_request_target workflows are one misconfiguration away from becoming a publish oracle. The TanStack ecosystem fits that profile exactly.
The attack lasted six minutes. The cleanup will take longer. The structural fix, which is that registries need to verify not just the identity of the publisher but the integrity of the publish trigger, has not been shipped by anyone yet.
Frequently Asked Questions
Does this OIDC trust-model weakness apply to registries outside npm?
Yes. PyPI and RubyGems both adopted OIDC trusted publishing through the same GitHub Actions integration in 2023-2024. The trust delegation is identical: the registry trusts the workflow, not a stored credential. Any project on those registries using pull_request_target with OIDC publish permissions is vulnerable to the same abuse chain, because the underlying assumption (the workflow is uncompromised) is shared across all three registries.
Why didn’t OIDC’s short-lived tokens limit the damage?
Traditional token theft is a one-time event: the credential is used and eventually rotated. Cache poisoning is persistent. The TanStack attacker used it to ensure malicious code survived across multiple workflow runs, even after the triggering PR was closed. Each new run extracted a fresh OIDC token from the compromised environment, so the tokens’ short lifetime provided no defense. The attacker needed only one successful publish window per package.
What breaks if teams run npm install --ignore-scripts in CI?
Packages like esbuild, swc, and sharp rely on postinstall hooks to compile or download native binaries. Disabling scripts globally breaks those builds. The operational fix is --ignore-scripts paired with an explicit allowlist or selective npm rebuild for known-safe packages, which adds maintenance overhead proportional to the number of native dependencies in the project.
Could npm have caught the 84-package publish burst automatically?
The attacker published exactly two versions per library. npm’s fraud detection flags unusual publish velocity, but velocity alone is a weak signal when the publisher identity is legitimate and the rate could plausibly match a maintainer batch-publishing across a monorepo. The signal that would have caught it, whether the publish was triggered by an authorized maintainer’s merge, is not something the registry currently measures.
What would a structural fix at the registry level require?
One proposed approach is requiring OIDC publish claims to include a signed attestation of the triggering Git ref, so the registry can reject publishes originating from commits not on the repository’s default branch. This would have blocked the TanStack attack, since the malicious commit came from a fork, but it would also prevent legitimate publishes from feature branches, forcing maintainers to change their release workflows.