אִם יִרְצֶה הַשֵּׁם
Aikido Security's malware team caught 373 malicious package-version entries spread across 169 npm namespaces on May 12, 2026. The hit list: @tanstack (React Router alone pulls 12m+ weekly downloads), @mistralai, @uipath, @squawk, and a dozen others. TeamPCP, the cloud-focused criminal group behind this, broke nothing cryptographic. No maintainer got phished. They landed on one CI runner, and the ecosystem's own machinery did the propagation from there.
The name comes from April 2026, when the same group hit SAP packages. Same worm, bigger sandbox.
Taxonomy First
Under the Bilar (2009) nth-Order Attack Framework, this is a Second-Order attack. Two ancillary integrity layers the defender explicitly relies on were both subverted:
Hop 1 -- npm registry. You assume a scoped package under @tanstack was published by the @tanstack organization. The registry enforces no behavioral verification of artifacts. It checks that the publishing token has namespace rights. Full stop.
Hop 2 -- Publisher CI/CD + GitHub Actions OIDC. You assume OIDC-authenticated publishes originate from clean build environments under organizational control. GitHub's trusted publishing mechanism checks that the publish request came from a repo-scoped OIDC claim with id-token: write. It does not check whether the runner making that request already ran hostile code during npm install five minutes earlier.
Both assumptions violated. Patient zero, the initial vector that seeded the first malicious version, remains an intelligence gap. What follows describes propagation mechanics once the worm is live.
The Propagation Loop, Step by Step
The worm ships three payload files inside otherwise normal-looking tarballs: router_init.js, router_runtime.js, and tanstack_runner.js. The malicious dependency entry points to @tanstack/setup, which resolves to an orphan commit on GitHub -- a branch with no parent, invisible to normal repo history traversal.
When a developer or CI runner does npm install:
-
npm executes the
preparelifecycle hook automatically, before the install completes. No flag required. No prompt. This is documented npm behavior. -
preparecallsbun run tanstack_runner.js. Bun is invoked because it is faster than Node for script execution and because its presence in a CI log is less likely to trigger alerts than a Node subprocess spawned from a lifecycle hook. -
tanstack_runner.jsharvests credentials from the host environment:~/.npmrc(npm tokens),~/.gitconfigand environment variables for GitHub PATs, AWS/GCP/Azure instance metadata via169.254.169.254(IMDS), Kubernetes service account tokens from/var/run/secrets/kubernetes.io/serviceaccount/token, and HashiCorp Vault tokens fromVAULT_TOKENenv. All of this exfiltrates over Session's P2P network (filev2.getsession.org), which is encrypted and decentralized, making egress filtering harder than blocking a single C2 IP. -
With a GitHub PAT in hand, the worm calls GitHub's OIDC token endpoint. Because the runner has
permissions: id-token: writein the workflow (standard for tokenless publishing setups), GitHub hands back a valid, short-lived OIDC JWT scoped to that repository. -
That JWT goes to npm's OIDC exchange endpoint. npm mints a publish-capable token for the victim's namespace. The worm publishes new malicious versions of the victim's own packages, embedding the same three payload files.
-
Those new versions carry valid SLSA Build Level 3 provenance attestations via Sigstore. The attestations are cryptographically legitimate. The CI runner was real. The build environment was real. The Rekor transparency log entry is real. The artifact is malicious.
-
For persistence, the worm writes to
.claude/settings.jsonand.vscode/settings.json. It spoofs commits as the Anthropic Claude bot to obscure its presence in git history. It also creates npm registry tokens with embedded ransom notes -- revoke the token, the note threatens, and your machine gets wiped.
The loop closes when the next developer installs one of the victim's newly-published packages and the cycle repeats.
Why SLSA Failed Here
SLSA Build Level 3 is supposed to guarantee that an artifact came from a specific, trusted build platform, with the source pinned and the build process isolated. It does all of that. The Sigstore attestation on these packages correctly records: built on GitHub Actions, from this repo, at this commit, by this workflow.
What SLSA does not attest to is the state of the build environment's inputs. The CI runner that produced the attestation had already executed hostile code during the npm install phase of its own build. SLSA's threat model assumes the build platform is trustworthy. The malware made the build platform the attack surface. The attestation verified the process wrapper, not the content being wrapped.
This is the first documented case of malicious npm packages carrying valid SLSA BL3 provenance. The Sigstore logs are not wrong. The chain of custody is correctly recorded. The artifact is still malicious. Provenance tells you where something came from. It does not tell you whether the origin was clean.
The Four Structural Failures (Emergent Insecurity Framework, arXiv:2509.11173)
Semantic Gap. The abstraction: "SLSA-attested package from a known publisher namespace, published via OIDC, is a trusted artifact." The implementation: the attestation pipeline verifies runner identity, not runner state. The OIDC exchange verifies repo scope, not execution history. The optimization for frictionless, automated publishing removed every checkpoint that could have caught the behavioral deviation.
Micro-State Weaponization. The prepare hook runs in under a second. One deterministic execution point, present in millions of CI pipelines. The system's response to it -- full host filesystem access, no secret isolation, automatic OIDC minting rights -- converts a single-line trigger into a full credential harvest. The lever is the hook. The amplification is everything the runner has access to.
Trusted Process Subversion. npm install is not optional. Every JavaScript project runs it. The payload is inert until that moment. All prior security scans -- SAST, registry scanning, dependency review -- ran on the tarball before install. The activation happens after the last checkpoint. The defender's own standard procedure is the detonation mechanism.
Systemic Latent Risk. The conditions here are ecosystem defaults: lifecycle scripts on by default, CI runners holding publish credentials, OIDC tokens mintable from any repo-scoped claim, transitive dependency graphs averaging hundreds of packages per project. No dedicated adversary required to find the next instance. One compromised node, and the propagation logic is baked into the toolchain itself.
Detection
Search your environments:
- Namespaces:
@tanstack/,@squawk/,@uipath/,@mistralai/,@tallyui/,@beproduct/,@draftlab/,@draftauth/,@taskflow-corp/,@tolka/ - Files:
router_init.js,router_runtime.js,tanstack_runner.jsanywhere innode_modules - CI logs:
bunprocess spawned duringnpm installornpm ciphases - Network: outbound to
filev2.getsession.org, any request to169.254.169.254from a build runner - Persistence: unexpected entries in
.claude/settings.json,.vscode/settings.json, or git commits attributed to the Anthropic Claude bot that you did not make
Operational Response
If any affected package ran in your environment, treat every secret the runner touched as compromised. That means npm tokens, GitHub PATs, AWS/GCP/Azure instance credentials, Kubernetes service account tokens, and Vault tokens. Rotate all of them before doing anything else.
Audit npm publish history for your organizational namespaces for the past 90 days. Review GitHub Actions OIDC trust policies: look for workflows with id-token: write that lack explicit subject claim constraints (sub should bind to a specific branch, tag, or workflow file digest -- not just the repo name).
Structural Fixes
ignore-scripts=true in .npmrc across all CI runners and developer machines. Set it at the organization level via .npmrc in the repo root or via npm config in your CI image. Run necessary build steps through an explicit, audited script runner, not the package manager's automatic hook machinery.
Separate the install environment from the release environment. A runner that executes npm install should hold no publish tokens, no cloud credentials, no PATs. The install phase is now an adversarial execution surface. Publish credentials belong in a dedicated release pipeline, ideally with HSM-backed key storage and a human approval gate.
Digest pinning. package-lock.json integrity hashes are not enough if the registry can serve a different tarball for the same version. Pin the exact resolved URL and integrity field, and verify against an internal artifact mirror rather than the live public registry.
OIDC subject claim constraints. GitHub's OIDC sub claim can be restricted to a specific branch, tag, environment, or workflow file digest. repo:org/repo:ref:refs/heads/main is loose. repo:org/repo:environment:production tied to a protected environment with required reviewers is tighter. Bind publish-capable OIDC claims to the narrowest possible subject.
Internal registry quarantine. Incoming external packages land in an isolated tier. Automated installs run in sandboxed environments (gVisor or Firecracker) with no host secret access and monitored network egress. After 72 hours with no anomalous behavior, packages promote to the internal trusted index. Nothing from the public registry goes directly into a build.
Post-install behavioral verification. Before any package is consumed in a build, run it in a sandboxed, instrumented environment and observe: network connections, filesystem writes outside node_modules, subprocess spawning, environment variable reads for tokens and credentials. The package manager's install log tells you what ran. It does not tell you what it did.
The Actual Lesson
The cryptography worked. The provenance pipeline worked. The OIDC federation worked. All of it worked exactly as designed, and the artifact was still malicious.
The gap is between verifying where something came from and verifying what it does. Every optimization in this stack -- tokenless auth, automated attestation, lifecycle hooks, transitive dependency resolution -- was chosen for good engineering reasons. Each one removed friction. The friction that got removed was the friction that would have caught this.
The defense requires changing what you trust, not just adding more verification of the things you already trust. Behavioral attestation over identity attestation. Secret compartmentalization at install boundaries. Verification of the final running artifact, not the signed tarball. The signature tells you the chain of custody. It does not tell you the chain was clean.