The First npm Malware With Valid SLSA Provenance: How Mini Shai-Hulud Hijacked TanStack Without Stealing a Credential

Mehmet Akif Mehmet Akif
Jun 18, 2026 12 min read 48 views
Share:
The First npm Malware With Valid SLSA Provenance: How Mini Shai-Hulud Hijacked TanStack Without Stealing a Credential

The Package Was Signed, Attested, and Built by a Trusted Pipeline. It Was Also Malware.

On May 11, 2026, between 19:20 and 19:26 UTC, 84 malicious versions across 42 @tanstack/* packages landed on the npm registry. @tanstack/react-router alone pulls more than 12 million weekly downloads, so the blast radius was immediate and enormous. The part that should change how you think about supply chain defense is this: no npm token was stolen, the publish workflow was never modified, and every one of those poisoned packages shipped with valid SLSA Build Level 3 provenance. The cryptographic attestation was accurate. The packages really were built by TanStack's own pipeline, signed through the legitimate Sigstore stack, and published under TanStack's trusted OIDC identity. They just had a 2.3 MB credential stealer injected into the tarball at build time.

This is CVE-2026-45321, CVSS 9.6, and it's the fourth major wave of the Shai-Hulud worm, this iteration called Mini Shai-Hulud and attributed by StepSecurity to the group TeamPCP. It's also the first documented case of a malicious npm package carrying a valid provenance attestation. For everyone who spent the last two years being told that Sigstore signing and SLSA provenance were the answer to supply chain attacks, this is the incident that says: not by themselves, they aren't.

The attack matters for three separate reasons, and they don't usually appear together. The initial access needed no stolen credential. The malware self-propagates like a worm. And the artifact it produced was indistinguishable from a legitimate one by every automated trust signal the ecosystem currently has. Each of those is worth unpacking.

Track Threat Intelligence like this every Monday.

Every Monday, the 5 threats SOC teams can't afford to miss — with analyst commentary.

How Do You Compromise a Pipeline Without Stealing Anything?

Every prior Shai-Hulud wave started with a phished or stolen credential. The TanStack attack started with three GitHub Actions misconfigurations chained together, none of which was sufficient on its own. The TanStack team's postmortem is unusually thorough, and the chain is worth walking through because the same pattern exists in a large number of open-source CI setups right now.

The setup happened first. On May 10, the attacker forked TanStack/router and renamed the fork to zblgg/configuration, a deliberate choice to keep it out of GitHub's fork-list searches. They authored a malicious commit under a fabricated identity, claude <[email protected]>, impersonating the Anthropic Claude GitHub App, and prefixed it with [skip ci] to suppress automated checks on push. Quiet staging, designed to look like automated tooling activity.

Then the three-link chain.

The first link was a Pwn Request. TanStack's bundle-size.yml workflow ran on the pull_request_target trigger for fork pull requests. That trigger is the dangerous one: unlike pull_request, pull_request_target executes in the context of the base repository, with access to the base repo's secrets and a privileged GITHUB_TOKEN. The workflow then checked out the fork's PR-merge ref and ran a build on it:

 
 
yaml
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   # fork's code
      - uses: TanStack/config/.github/setup@main    # transitively calls actions/cache
      - run: pnpm nx run @benchmarks/bundle-size:build   # executes fork-controlled code

Checking out and executing untrusted fork code inside a privileged trigger context is the textbook Pwn Request, and GitHub's own Security Lab has warned about it for years. But on its own, running attacker code in a sandboxed runner that gets torn down isn't enough to publish a package.

The second link was GitHub Actions cache poisoning across the fork-to-base trust boundary. The Actions cache is shared across that boundary in a way most maintainers never think about. The attacker's fork code, running under the privileged trigger, wrote a malicious pnpm store into the cache. That poisoned cache then sat there, waiting.

The third link was runtime OIDC token extraction. When a legitimate maintainer later merged a real PR and triggered the release workflow, that workflow restored the poisoned cache. The attacker's planted binaries executed during the test and cleanup phase and read the OIDC token directly out of the GitHub Actions runner's process memory via /proc/<pid>/mem. With that token, the malware authenticated to npm through TanStack's legitimate OIDC trusted-publisher binding and published the poisoned versions. The publish workflow itself was never touched. The attacker borrowed its identity mid-run.

That's why no credential was stolen in the conventional sense. There was no npm token sitting in a secrets store to exfiltrate. The OIDC token is ephemeral, minted at runtime for the workflow, and the attacker simply read it out of memory during a window where it was valid and used it before it expired. Each affected package got exactly two malicious versions, published a few minutes apart, all inside a six-minute window.

Why Valid Provenance Didn't Help

SLSA provenance and Sigstore attestation were built to answer one question: was this artifact built from the source it claims, by the pipeline it claims? In this attack, the honest answer to that question is yes. And that's the problem.

The provenance attestation certifies the build's origin, not the build's integrity. The malware was injected into the tarball during a legitimate build run, executed by the real pipeline, attested by the real Sigstore stack, under the real OIDC identity. Build Level 3 provenance says "this came from TanStack's CI." It was telling the truth. The package came from TanStack's CI. The attestation has no way to express "and also there was attacker-controlled code running in that CI at build time."

This is the operationally important takeaway, and it's worth stating plainly because a lot of supply chain guidance over the past two years implied otherwise. A green provenance badge is not a safety signal. It's an origin signal. Running npm audit signatures on these packages returns valid signatures. The attacker used stolen OIDC tokens with the legitimate Sigstore infrastructure to produce genuine Build Level 3 attestations. If your supply chain policy is "only install packages with valid provenance," these packages pass that policy.

The defensive value of provenance isn't zero. It narrows where a compromise can originate and gives you a cryptographic audit trail. But it verifies the pipeline, and if the pipeline is the thing that got hijacked, the attestation inherits the compromise and signs it. Treating attestation as the terminal trust decision is the mistake.

What the Payload Actually Does on the Install Host

The implant shipped as router_init.js (it copies itself to router_runtime.js to blend in with previous waves), roughly 2.3 MB, obfuscated with the javascript-obfuscator pattern: string-array rotation, hex-encoded identifier lookups like _0x253b, and control-flow flattening. Because it runs as part of the npm install lifecycle, anyone who installed an affected version on May 11 has to treat the install host as compromised. That includes CI runners, not just developer laptops.

On execution, it fingerprints the environment (OS, CI platform, JS runtime) and then sweeps every secrets plane available in a modern cloud-native CI environment, using both direct environment-variable reads and active API calls. The harvest list reads like an inventory of everything that matters: GitHub tokens, npm tokens, and SSH keys; AWS, GCP, and Azure credentials and cloud identities; Kubernetes service-account tokens and HashiCorp Vault secrets; and CI/CD secrets from GitHub Actions, CircleCI, and related platforms.

Then it self-propagates, which is what makes it a worm rather than a one-shot stealer. Using the stolen tokens and the OIDC token-exchange mechanism, it publishes trojanized versions of any package the compromised pipeline or maintainer can access. One compromised pipeline becomes dozens. By end of day on May 11, the worm had spread well beyond TanStack to @uipath/* (66 packages), @mistralai/*, the official @opensearch-project/opensearch client, 19 @squawk/* aviation packages, intercom-client, and more, totaling 373 malicious versions across 169 npm packages plus PyPI packages. Cumulative downloads in the affected window were estimated north of 518 million.

The exfiltration design is what defenders need to internalize, because it breaks domain-based blocking. The worm uses a triple-channel C2: a typosquat domain (git-tanstack[.]com), the decentralized Session messenger network reached through getsession.org seed nodes (with the full Session protocol stack, including the signalservice Protocol Buffers schema, embedded directly in the payload), and GitHub API dead drops where the stolen tokens create Dune-themed repositories carrying the description "Shai-Hulud: Here We Go Again." Block the typosquat domain and two redundant channels remain, one of them a decentralized network with no central endpoint to sinkhole.

There's a destructive component with a trigger condition worth knowing. The payload installs a persistence daemon named gh-token-monitor via a macOS LaunchAgent or a Linux systemd user service. It polls GitHub every 60 seconds. If a poll returns a 40X error, which happens when the stolen token gets revoked, the monitor attempts rm -rf ~/. The daemon auto-exits after 24 hours without firing the destructive handler. The interaction here is nasty: revoke the token carelessly and you can trigger the wipe, which is why the remediation order matters. And as with prior Mini Shai-Hulud variants, it checks whether the system is configured for the Russian language and terminates without exfiltrating if so.

What Detection and Remediation Look Like

The remediation sequence is order-sensitive because of the rm -rf ~/ trigger, so this is one of the rare cases where the steps have to happen in a specific order rather than in parallel.

Remove the persistence daemon before revoking anything. Check ~/Library/LaunchAgents/com.user.gh-token-monitor.plist on macOS and ~/.config/systemd/user/gh-token-monitor.service on Linux, and remove the daemon first so that token revocation doesn't return the 40X that arms the destructive handler. Then check .claude/ and .vscode/ directories for persisted payload files like router_runtime.js and setup.mjs, which survive npm uninstall and are how the implant re-establishes itself.

Only after the daemon is gone do you rotate credentials, and you rotate everything reachable from the install host: npm tokens, GitHub PATs, AWS/GCP/Azure credentials, Kubernetes service-account tokens, Vault secrets, SSH keys, and CI/CD secrets. Run npm token list and revoke anything unrecognized.

For network detection, block the known C2 at the DNS and proxy layer: git-tanstack[.]com, *.getsession.org, filev2.getsession.org, api.masscan.cloud, and 83.142.209[.]194. Then audit GitHub Actions runs that occurred after 2026-05-11T19:20Z for unexpected npm publish events and outbound connections to the Session or masscan infrastructure. If any of your own packages were published during a CI run that had installed a compromised dependency, those published versions may themselves be trojanized, because that's exactly how the worm propagates.

A few detection signals generalize beyond this specific campaign and are worth building into standing controls.

The size-jump heuristic is the cheapest reliable signal. In the related June 1 Miasma wave against the @redhat-cloud-services namespace, the attacker replaced an approximately 200 KB index.js with a 4.29 MB obfuscated payload, a 25x increase. A CI check that flags a sudden multiple-x growth in a package's main entry file between versions catches this whole class of injection cheaply, because the obfuscated stealer is large and the legitimate file isn't.

Lockfile integrity is the structural control. Pin integrity hashes for your dependencies in package-lock.json or pnpm-lock.yaml and fail CI on any hash mismatch. A poisoned version republished under the same package name will not match the pinned hash.

OIDC scope restriction is the control that addresses the root cause. Set permissions: id-token: none in every workflow that doesn't explicitly need OIDC publishing, and pin id-token: write to only the specific job that publishes. The TanStack attack worked because an OIDC token was available in a runner context where attacker code could read it. Minimizing where that token exists shrinks the window. And don't run untrusted fork code under pull_request_target with a checkout of the PR merge ref, which is the Pwn Request precondition.

Trust signal What it proves What it does not catch
SLSA provenance / Sigstore attestation Package was built by the claimed pipeline Malware injected during a hijacked legitimate build
npm audit signatures Signature is cryptographically valid A genuine signature produced with a stolen OIDC token
Provenance badge on registry Origin is attested Build-time integrity of that origin
Pinned lockfile integrity hash Bytes match a known-good version Nothing, if you never had a known-good baseline
Package entry-file size delta Anomalous code volume change Small, surgical payloads under the size threshold

The Part That Should Worry You Most

TeamPCP briefly published the Shai-Hulud source code on GitHub on May 12 before the repository came down. Copies were mirrored immediately. The Miasma payload that hit @redhat-cloud-services on June 1 is derived directly from that open-sourced Mini Shai-Hulud code, with substantially identical tradecraft, and attribution is already getting harder because copycats are running the same toolchain. This is the Mirai pattern: once the source is public, the technique stops belonging to one group and becomes ambient.

The structural conditions that made TanStack work are not unique to TanStack. A large share of open-source projects run pull_request_target workflows that check out and build fork code. Many of them have OIDC publishing wired into the same repository. Actions cache is shared across the fork-to-base boundary by default. The three ingredients sit in countless CI configurations right now, and the worm that chains them is now public code. Yesterday, June 17, an attacker compromised the @mastra npm organization and added a dayjs typosquat as a dependency across more than 140 packages in the Mastra AI framework, over a million weekly downloads exposed in a single push. Different entry point, same ecosystem-as-force-multiplier outcome.

The thing to sit with is what this does to the trust model developers have operated under. The instinct has been that a signed package from a reputable maintainer, built by a real CI pipeline, carrying valid provenance, is safe to install. Every one of those properties was true of the TanStack packages, and they were malware that swept cloud credentials out of every CI runner that installed them. The attestation didn't lie. It just answered a different question than the one anyone was actually asking, and the gap between those two questions is where

Mehmet Akif

Mehmet Akif

CTI Analyst

CTI Digest · Every Monday, 9:00 (Europe/Istanbul)

Track Threat Intelligence threats like this — every Monday.

Every Monday, the 5 threats SOC teams can't afford to miss — with analyst commentary.

Comments (0)

Leave a Comment

* Required fields. Privacy Policy