what actually happened
On May 11, 2026, the Mini Shai-Hulud worm pushed 84 malicious versions across 42 @tanstack/* packages to npm in roughly six minutes, and the detail that should keep you up at night is that every single one of those versions carried a valid SLSA Build Level 3 provenance attestation, verifiable against Sigstore, traceable back to a real GitHub Actions workflow run on the real repository. Nobody stole a maintainer's npm token. Nobody phished anyone. There was no leaked classic automation token sitting in a gist somewhere, no compromised laptop, none of the boring stuff we usually post-mortem. The attacker walked in through the front door of the release pipeline itself and got the pipeline to sign the malware on their behalf, which means the cryptographic chain of custody that everyone has spent three years building out did exactly what it was designed to do. It attested, faithfully and correctly, that this code was built by this workflow in this repo. The code was just poison. If your mental model of supply chain security is "provenance proves the artifact came from the source, therefore the artifact is trustworthy," this attack is a direct refutation of that model, and the uncomfortable part is that the verification step never failed. It passed. Loudly and cryptographically. We've spent client engagements at steezr ripping apart CI configs for fintech and SaaS teams who genuinely believed their Sigstore wiring was the finish line, and this incident is the cleanest argument I've seen that signing the wrong thing correctly is worse than not signing at all, because it manufactures confidence.
the pull_request_target trap
The entry point was a workflow triggered on pull_request_target, which is the single most misunderstood trigger in all of GitHub Actions and the source of a category of bugs the security folks have been calling Pwn Requests since 2020. The difference between pull_request and pull_request_target matters enormously and almost nobody internalizes it until it bites them. A normal pull_request workflow runs in the context of the fork, with no access to your repository secrets, which is safe and boring. pull_request_target runs in the context of the base repository, with full access to secrets, the GITHUB_TOKEN with write permissions, the whole vault, and it was added so maintainers could label PRs and post welcome comments on contributions from forks without handing secrets to untrusted code. The problem is the moment your pull_request_target workflow does an actions/checkout with ref: ${{ github.event.pull_request.head.sha }} and then runs anything from that checked-out tree, an install script, a lint step, a test runner, a npm ci that executes a postinstall hook from the attacker's package.json, you have just executed attacker-controlled code inside a privileged context. The TanStack workflow did exactly this. A contributor PR, a checkout of the fork's head, and a build step that ran code from the fork. From there the attacker had arbitrary execution inside a runner that could touch the cache and, more importantly, could reach the OIDC token minting endpoint. This is not an exotic zero-day. It's a config shape that thousands of repositories ship today, and you can find it in your own org with a grep for pull_request_target across your .github/workflows directory followed by a hard look at whether any checked-out fork code ever runs.
cache poisoning and OIDC extraction
Arbitrary execution in a runner is bad but it's not automatically game over, so the worm chained two more steps to turn a single poisoned PR run into a self-propagating release. The first was GitHub Actions cache poisoning. The actions/cache mechanism keys entries by branch and a cache key, and there's a long-known scoping quirk where a cache written from a feature branch or a PR context can be restored by workflows running on the default branch, which means the attacker wrote a tampered dependency artifact, a malicious node_modules tarball, into the cache from the poisoned PR run, and then waited for the legitimate release workflow on main to restore it. The release pipeline pulled the poisoned cache, built against it, and produced artifacts that were structurally signed and provenance-attested as legitimate. The second step was OIDC token extraction. The release workflow requested an OIDC token to authenticate to npm for trusted publishing, and because the attacker's code was already resident in the runner, they scraped the token out of runner memory and the ACTIONS_ID_TOKEN_REQUEST_URL environment variables, then used it to publish. The npm trusted publishing flow saw a valid OIDC token scoped to the real repo and the real workflow, accepted the publish, generated the provenance, and signed everything. Every link in that chain behaved correctly according to its own spec. The cache returned what it was asked for. OIDC minted a token for the workflow that asked. npm trusted the publisher it was told to trust. The failure lives in the spaces between these systems, in the assumption each one makes about the trustworthiness of the runner it shares with everything else.
why provenance is a false floor
Provenance answers exactly one question: did this artifact come out of this build process. It says nothing about whether the build process was clean, whether the inputs were poisoned, whether the runner was shared with hostile code minutes earlier, or whether the workflow that produced it should have been allowed to run at all. SLSA Build Level 3 specifically promises non-falsifiable provenance and isolated build environments, and the cruel irony of the TanStack incident is that the build environment was technically isolated in the sense SLSA means it, a fresh ephemeral runner, no persistent state between unrelated jobs, but the cache crossed that boundary as a deliberate feature and the contributor PR crossed it as a deliberate feature, and SLSA has nothing to say about either because they're not part of its threat model. We keep watching teams treat a green provenance check as a binary safe/unsafe verdict, the way people once treated a valid TLS certificate as proof a website was legitimate, and it's the same category error. A cert proves you're talking to who you think you're talking to. It does not prove they're not a phishing site. Provenance proves the artifact is what the pipeline produced. It does not prove the pipeline wasn't subverted. The signing infrastructure is genuinely useful, I'm not arguing you should rip out Sigstore, but if it has quietly become the thing your team points to when someone asks "is our supply chain secure," you've built a confidence machine that produces false negatives under exactly the attack class that's now in the wild and replicating.
defend the workflow shape, not the signature
The real defense lives upstream of signing, in the shape of the workflow, and it's unglamorous plumbing work that nobody gets to brag about at a conference. Start by auditing every pull_request_target workflow in your org and making an ironclad rule that none of them ever check out and execute fork-controlled code, and if you genuinely need to build a PR you split it into two workflows where the privileged half only ever reads metadata and the unprivileged half does the building with no secrets attached. Scope your secrets and OIDC permissions per job with permissions: blocks set to the minimum, never at the workflow level, because a release job needing id-token write should not share a runner phase with anything that ran untrusted code. Pin your actions to full commit SHAs rather than tags, since a tag like @v4 is mutable and an attacker who compromises an upstream action can repoint it. Treat the Actions cache as untrusted input when it crosses trust boundaries, and for release builds either disable cache restore entirely or use a separate cache scope that PR runs can never write to. Put your publishing step on its own dedicated, locked-down workflow that triggers only on tagged releases from protected branches with required reviews, so that the path to an npm publish is narrow, observable, and physically separated from the noisy contributor-facing CI. None of this needs a vendor. It needs someone to sit down for an afternoon with your .github/workflows directory and a hostile imagination, which is exactly the kind of unglamorous security work we end up doing for clients who assumed their setup was fine because the badges were green. The signature was never the floor. The workflow that earns the signature is.
