npm Package Poisoning Is Getting More Frequent: What the TanStack Incident Teaches Us
The painful part of the TanStack incident was not a stolen npm token. It was how several reasonable pieces of CI/CD trust were chained into a publish path.
The Short Version
npm package poisoning no longer looks like someone registering a typo-squatted package and waiting for a tired developer to mistype a name.
On May 11, 2026, 84 malicious versions were published across 42 @tanstack/* packages. According to Tanner Linsley's postmortem, the publishes happened between 19:20 and 19:26 UTC, and the incident was publicly reported by an external researcher roughly 20 minutes later.
The important part is what did not happen: this was not a classic stolen npm account story, and there is no evidence that npm credentials were stolen.
Instead, the attacker chained three things together:
- the
pull_request_target"Pwn Request" pattern; - GitHub Actions cache poisoning across the fork-to-base trust boundary;
- runtime extraction of an OIDC token from the GitHub Actions runner process, then direct publish requests to the npm registry.
In other words, the attacker did not steal a long-lived npm token. They borrowed a legitimate publishing identity from the release environment.
That is why this incident feels different. Supply-chain attacks are moving from "steal the credential" toward "abuse the automation that is allowed to mint one."
This Is Not Just a TanStack Problem
It would be easy to turn this into a neat little morality play about one project having one bad workflow.
I do not think that is the useful lesson.
TanStack is mature, widely used, and maintained by people who understand modern JavaScript infrastructure better than most teams do. That is exactly why the incident matters. The attacker did not take the most obvious path. They looked for the joints between CI/CD, caching, OIDC, and npm trusted publishing.
Many teams have a version of this setup today:
- fork PRs that automatically run checks;
- CI caches for pnpm, npm, or Yarn;
- release workflows with elevated permissions;
- npm trusted publishing through GitHub OIDC;
- dependency installs that allow lifecycle scripts;
- maintainers responsible for many packages at once.
Each of those choices can be reasonable on its own.
The problem is that supply-chain attacks are rarely polite enough to stay inside one box. They connect the boxes.
The Three Parts Worth Remembering
1. pull_request_target Is Not the Whole Problem
pull_request_target is not inherently evil. It is often used for labeling PRs, commenting on issues, and updating metadata because it runs in the context of the base repository.
The dangerous move is different: checking out fork-controlled code inside a pull_request_target workflow and then executing it.
That crosses a boundary. The workflow is running with base-repository context, but the code being executed came from an untrusted fork. GitHub Security Lab has warned about this pattern for years under the name "pwn request."
In the TanStack incident, the workflow attempted to split trusted and untrusted work. That instinct was right. The missed surface was the cache.
It is tempting to believe this is enough:
permissions:
contents: read
But actions/cache post-job saves are not simply controlled by the normal GITHUB_TOKEN permission block. A job that looks read-only can still mutate cache state inside the repository's cache scope.
That is a subtle failure mode, and subtle failure modes are where automation gets expensive.
2. Cache Is a Performance Feature and a Trust Boundary
We usually talk about cache as a speed optimization. That framing is too small.
In a modern frontend project, a package-manager cache may contain:
- package store data;
- compiled artifacts;
- native binaries;
- files generated by
postinstallorprepare; - dependency content later reused by release workflows.
The attack took advantage of that. A malicious PR polluted the pnpm store and saved it under a key that the release workflow would later restore. When a legitimate push to main triggered the release workflow, the poisoned cache crossed from untrusted PR execution into a trusted publishing environment.
The PR did not need to be merged.
It could be force-pushed back to a no-op, closed, and deleted. The cache still existed.
That is the part I keep coming back to. Caches can outlive the social signal that made us stop looking at the PR.
3. OIDC Is Not a Magic Shield
npm trusted publishing and GitHub OIDC are good ideas. Short-lived credentials are better than long-lived npm tokens sitting in CI secrets. Provenance is better than a hand-rolled publish step with no paper trail.
But OIDC solves a specific problem: reducing long-lived secret exposure. It does not make a compromised runner safe.
If a release workflow has id-token: write, and attacker-controlled code is already executing on that runner, the attacker can try to obtain a publish-capable identity from the runtime environment. In the TanStack incident, the normal "Publish Packages" step was skipped because tests failed. The malicious code still made direct publish requests to registry.npmjs.org.
That is the uncomfortable lesson.
We used to ask, "Was the npm token leaked?"
Now we also need to ask, "Can any code path in this workflow reach the publishing identity?"
Why npm Poisoning Keeps Showing Up
npm is not uniquely doomed. But the ecosystem has several properties attackers love.
First, dependency chains are long. One compromised package can spread through transitive dependencies into projects that never selected it directly.
Second, install time can execute code. preinstall, postinstall, and prepare scripts have legitimate uses, from native addons to generated clients to binary downloads. That legitimacy makes them hard to ban outright.
Third, maintainers are overloaded. A single maintainer may own dozens of packages. A single compromised publishing path can become a multiplier.
Fourth, CI/CD has become complicated. Release systems now involve cache layers, matrix builds, preview jobs, automatic releases, provenance, OIDC, comment bots, and third-party actions. Every layer improves productivity. Every layer also creates a new trust boundary.
Fifth, open source is transparent. That transparency is a strength, but it also lets attackers study workflows, cache keys, release cadence, and maintainer habits in public.
So no, the lesson is not "npm is unsafe" or "open source is unsafe."
The better lesson is this: open-source package infrastructure has become high-value production infrastructure, while many of our habits still assume the main thing to protect is a static token.
That mental model is no longer enough.
What Regular Developers Can Do
If you installed one of the affected TanStack versions on May 11, 2026, the official recommendation was to treat the install host as potentially compromised and rotate reachable AWS, GCP, Kubernetes, Vault, GitHub, npm, and SSH credentials.
That sounds severe, but it matches the payload's goal: harvesting credentials from the environment where install scripts run.
For day-to-day work, I would turn a few practices into muscle memory.
1. Pay Attention to Strange Version Bursts
If a package suddenly publishes several patch versions in quick succession, especially without clear release notes, pause for a minute.
You can do a quick check with:
npm view @tanstack/history versions --json
npm diff --diff=@tanstack/history@1.161.8 --diff=@tanstack/history@1.161.9
Manual review will not catch every malicious payload. But it can catch strange files, strange dependencies, and unexpected lifecycle scripts.
2. Treat the Lockfile as a Security Artifact
pnpm-lock.yaml, package-lock.json, and yarn.lock are not just reproducibility files. They are audit surfaces.
After an incident, do not only inspect package.json. Check which versions were resolved, which tarballs were fetched, and whether any GitHub dependencies appeared unexpectedly.
3. Be More Explicit About Install Scripts
Disabling all scripts is not realistic for every project. But trusting every install script by default is becoming harder to defend.
For some CI phases, this is worth considering:
pnpm install --ignore-scripts
When a project genuinely needs build scripts, the allowed packages should be explicit. pnpm's newer build-approval direction is a healthy sign here: installation should not be an unlimited execution surface by default.
4. Reduce Horizontal Credential Exposure in CI
Install jobs, test jobs, and publish jobs should not all have access to the same cloud credentials or publishing identities.
A simple rule works surprisingly well: if a secret is not needed during install or test, do not make it available there. If a permission is only needed during publish, open it only during publish.
What Maintainers Should Audit
If you maintain npm packages, especially packages with automated publishing, the TanStack incident is a good reason to inspect a few things.
1. Search for pull_request_target
The question is not "should we ban it forever?" The question is "what exactly does it do?"
Safer uses usually look like:
- read PR metadata;
- apply labels;
- leave comments;
- do not checkout fork code;
- do not execute fork code;
- do not write cache entries later reused by release workflows.
If you need to run fork-controlled code, prefer pull_request and accept the constraints: first-time contributor approval, lower permissions, and isolated cache behavior.
2. Do Not Let Untrusted PRs Write Production Cache
A cache key is not just a string. It decides which jobs share state.
For fork PRs, consider:
- disabling cache saves;
- scoping cache keys to PR-only contexts;
- keeping release workflow caches separate;
- purging caches during incident response;
- documenting cache cleanup as part of the security playbook.
The boring operational detail matters because cache survives longer than the workflow run that created it.
3. Introduce Publishing Identity at the Last Moment
id-token: write should not be ambient across the entire workflow.
A sturdier shape is a separate publish job that only runs after tests, builds, and package-content validation pass. That job should not restore untrusted cache content, and it should not re-run install scripts that do not need to run.
This does not make publishing invincible. It does reduce the amount of workflow surface that can reach a publish-capable identity.
4. Pin Third-Party Actions
Floating refs are convenient: @main, @v5, @latest.
On release paths, that convenience is a standing supply-chain dependency on someone else's moving target. Pin critical third-party actions to commit SHAs. Upgrade them deliberately.
Slower is fine. Unbounded is not.
5. Monitor Your Own Publishes
TanStack was lucky in one important way: external researchers noticed quickly, and the public IOC pattern emerged fast.
But third-party discovery should not be the only alert.
Maintainers can monitor for:
- unexpected package versions;
- new
optionalDependencies; - unusually large files in tarballs;
- unexpected GitHub dependencies;
- npm provenance from the wrong workflow;
- unusual install errors or download spikes after a release.
Even a small post-publish validation script is better than finding out from a stranger's issue.
The Part That Stuck With Me
The phrase that kept coming back while reading the postmortem was this:
We confuse successful automation with safe automation.
CI is green. The release is automated. Provenance exists. Trusted publishing is configured. Cache restored successfully. Everything looks modern.
But attackers do not care whether our setup looks like best practice.
They care about:
- which code gets executed;
- which cache gets reused;
- which job has elevated permissions;
- when a token appears at runtime;
- which maintainer is too busy to manually inspect a release;
- which install script will run by default.
That is what makes supply-chain security uncomfortable. It is not usually one dramatic bug. It is the composition of reasonable systems.
Closing Thought
I do not think the answer is "stop using npm."
I do not think the answer is "stop using OIDC."
And I definitely do not think every open-source project can suddenly operate like a large security team. That would be unrealistic and unfair.
But we do need a better mental model.
npm package poisoning is no longer only a package-manager problem. It is a CI/CD problem, an identity problem, a cache problem, a release-governance problem, and a maintainer-load problem.
When I look at an automated publish workflow now, I want answers to three questions:
- Can untrusted code influence the trusted release environment?
- Does cache cross the fork, main, and release trust boundaries?
- Does the publishing identity appear only at the final publish step?
If those answers are fuzzy, the risk is not theoretical.
Open source runs on trust. But in 2026, trust cannot only mean "everyone involved is well intentioned." It also has to mean boundaries, evidence, auditability, and a little more suspicion than feels comfortable.
That may be one of the core frontend engineering lessons from this new wave of npm package poisoning.