In May 2026, a worm called Shai-Hulud compromised 42 TanStack packages — including @tanstack/react-router, a library sitting in millions of JavaScript projects. It was live for about 3 hours. That was enough. If you installed dependencies that day, you may have been affected without knowing it. This post isn't for the people who maintain those libraries. It's for the rest of us — the developers who just use them.
"Fun fact" 1
It was live ~3 hours.@tanstack/react-routeralone gets 12.7 million weekly downloads. Meaning that it had ~225K downloads in the ~3 hour window — just forreact-router. That's one package. The attack hit 42@tanstack/*packages total.
Supply chain attacks used to feel like someone else's problem. A big library gets compromised, the maintainers fix it, life goes on. The Shai-Hulud worm changed that framing. It spread automatically to every package its victims maintained, turning regular developers into unwilling distributors of malware. Here's what happened, and what you can do today to reduce your exposure.
What happened?
The workflow used
pull_request_target, which runs with the base repo's trusted permissions — including access to secrets and a build cache shared with the real release pipeline.But it also checked out and executed the fork's code for benchmarking purposes. That's the dangerous mix: a stranger's code running with your own repo's trust.
The attacker didn't need to steal anything immediately. They just poisoned the shared cache and waited. Hours later, the legitimate release pipeline ran, picked up the tampered cache without knowing it, and published the malicious packages itself — using TanStack's own valid credentials.
The key insight: the misconfiguration wasn't obvious. The benchmarking intent was reasonable; the mistake was not realizing that pull_request_target + "run the PR's code" is always a dangerous combination regardless of what you're trying to do with it.
"Fun fact" 2
The worm had a dead-man's switch. It planted a background service that polledapi.github.com/userwith the stolen GitHub token every 60 seconds. If the token was revoked — meaning GitHub returned a 40x response — the service triggeredrm -rf ~/, wiping the user's entire home directory. You had to disable and remove the monitor service before revoking any credentials
The good news
Th pull_request_target Pwn Request specifically requires a public repo where strangers can open PRs.
The bad news
That said, the other parts of this attack (cache poisoning between workflows, OIDC token over-scoping) can still apply to private repos if your GitHub Actions workflows have similar misconfigurations
So far this sounds like a problem for library maintainers. It isn't. You don't need to maintain a library with millions of downloads to be exposed. You just need to run npm install on the wrong day at the wrong time. The moment a compromised package lands in your node_modules, you're part of the chain too.
How to prevent similar incidents?
min-release-age & ignore-scripts
NPM — .npmrc
min-release-age=7
ignore-scripts=true
-
min-release-age=7blocks packages published less than 7 days ago -
ignore-scripts=trueprevents lifecycle scripts likepreinstall/preparefrom running on install — which is exactly the vector the maliciousoptionalDependencyused. - ⚠️ npm CLI v11 is required. Upgrade to Node 24, or manually install npm v11.
pnpm (10.16) — pnpm-workspace.yaml
minimumReleaseAge: 10080 # minutes — 7 days
-
minimumReleaseAgeis on by default, it uses 1 day - Requires version 10.16 to avoid a bug that ignored this config.
- ignore-scripts: pnpm v10 stopped running
preinstall/postinstallscripts by default
Yarn Classic
- ❌
min-release-age— not available. No equivalent exists in v1. -
yarn install --ignore-scriptsexists but only as a CLI argument
Yarn Berry (v2+) — .yarnrc.yml
npmMinimalAgeGate: 10080 # minutes — 7 days
enableScripts: false # ignore-scripts equivalent
- ⚠️ Requires Yarn 4.10 and applies globally with no way to scope it
Dockerfile
ENV npm_config_min_release_age=3
-
npm cidoes respect.npmrcsettings and environment variables — thenpm_config_*convention works for bothinstallandci. - ⚠️ Still requires node v24 or npm v11, otherwhise
ENV npm_config_min_release_age=3in your Dockerfile will be silently ignored
Option A — Upgrade to Node 24 in Docker only
FROM node:24-alpine
# npm v11 is included, min-release-age works
ENV npm_config_min_release_age=3
Option B — Stay on your Node version, manually install npm v11
FROM node:22-alpine
RUN npm install -g npm@11
ENV npm_config_min_release_age=3
Sources
- https://snyk.io/blog/tanstack-npm-packages-compromised
- https://tanstack.com/blog/npm-supply-chain-compromise-postmortem
- TanStack commit history https://github.com/TanStack/router/commit/3ee179f0d9972173cb7510773fd26cb391b5fef5#diff-749feccfd42652f4a7571f0103a5b5b516b2b6d40f64f64200ac4c64870342aa
-
npm
min-release-agehttps://docs.npmjs.com/cli/v11/using-npm/config#min-release-age - npm + Node version relationship (confirms v11 ships with Node 24) https://nodejs.org/en/download/releases
-
pnpm
minimumReleaseAgehttps://pnpm.io/settings#minimumreleaseage - pnpm v11 release notes (confirms it's on by default at 1 day) https://pnpm.io/blog/releases/11.0
-
Yarn Berry
npmMinimalAgeGate(introduced in 4.10) https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate -
Yarn Berry
enableScriptshttps://yarnpkg.com/configuration/yarnrc#enableScripts - Cover Image: https://unsplash.com/photos/man-in-black-jacket-sitting-on-brown-rock-during-daytime-vIHwLieg-wo
Top comments (0)