Back

Blog

Insights

TanStack, Mistral AI, UiPath, OpenSearch, Bitwarden CLI: When Trusted Publishing and SLSA Both Verify, and the Publish Is Still Malicious

Minh Anh Day

It happened again — and this time the chain of trust verified end-to-end, on packages used by the entire AI and enterprise-automation industry.

Three weeks after we covered pgserve / CanisterSprawl, six weeks after axios, and seven after LiteLLM, on 11 May 2026 attackers published 84 malicious versions across 42 @tanstack/* packages — including @tanstack/react-router, which carries more than 11 million weekly downloads. Within hours the same worm had self-propagated to 169+ packages and 373 malicious versions across npm and PyPI (Snyk's count by end of day; Endor Labs reported 160+, Socket tracked up to 416 artifacts). The list of victims reads like an industry roll call:

  • @mistralai/mistralai — the official Mistral AI client SDK — plus @mistralai/mistralai-azure and @mistralai/mistralai-gcp.

  • 65+ @uipath/* packages, owned by the publicly listed enterprise-automation vendor.

  • @opensearch-project/opensearch, the official OpenSearch JavaScript client.

  • Bitwarden CLI, official SAP packages, Guardrails AI, @squawk/*, and dozens of others enumerated in CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx.

No npm token was stolen, no publish step was modified, no maintainer was social-engineered. Attackers abused a pull_request_target workflow in TanStack's pipeline, poisoned the GitHub Actions cache, and on the next legitimate push to main scraped the OIDC token out of the runner's memory at publish time. The malicious tarballs went up under TanStack's own trusted-publisher identity. Provenance attestations were generated, Sigstore signatures verified, and SLSA Build Level 3 was satisfied. This is the first documented case of a malicious npm package shipping with valid SLSA Build Level 3 provenance — and the worm doesn't stop there. The propagated payload calls generateKeyPairSync and signs its own SLSA-compatible Sigstore attestations on every package it republishes under a victim's identity. So when Mistral, UiPath, and OpenSearch packages got infected, those went out under each project's own valid OIDC publishes too, with valid-looking attestations the worm generated locally. Provenance proves where an artifact came from, not that the workflow execution path was trustworthy, and not that the resulting code is safe to run.

The install-time payload itself is the same Shai-Hulud worm family — attributed by multiple researchers to TeamPCP, same lineage as loaderio, qsuser, the original axios 1.14.1 / 0.30.4 cohort, and pgserve — but with several novel features: a memory-scrape OIDC theft step on the CI runner, the SLSA-attestation forgery on worm-propagated packages, Session/Oxen-network exfiltration, a .claude/ persistence sink aimed at coding-agent state, and a dead-man's-switch that wipes $HOME if you revoke the wrong token first. The PyPI side, carried in the Mistral cohort, goes further: Microsoft Threat Intelligence reports a transformers.pyz dropper impersonating Hugging Face, with explicit geofencing — dormant on Russian-language systems, and a probabilistic 1-in-6 rm -rf / on hosts identified as being in Israel or Iran. This is the first cohort in the lineage where the destructive payload is operational and shipping.

The chain reaction, updated to five

Eight weeks, five compromises, the same root cause every time: when a publisher's credentials or workflow are compromised, the entire chain of trust holds all the way down to the malicious code.

  • Trivy (16 March): the security scanner — the tool watching for backdoors — was itself backdoored.

  • LiteLLM (24 March): a credential harvester and Kubernetes worm shipped via PyPI, using credentials harvested from the Trivy compromise.

  • axios (31 March): a phantom dependency dropped a platform-specific RAT on every machine that ran npm install.

  • pgserve / CanisterSprawl (21 April): in-package JavaScript stealer with blockchain-anchored exfil and an npm-to-PyPI worm.

  • TanStack → Mistral AI → UiPath → OpenSearch → Bitwarden → SAP (11 May): the first incident where trusted publishing, OIDC federation, Sigstore, and SLSA Build Level 3 were used as designed and the publish was still malicious; the first to extract OIDC tokens from runner memory; the first to forge its own SLSA attestations on worm-propagated packages; and the first to ship an operational geo-targeted destructive payload.

Five different ecosystems and entry points; 169+ named projects compromised in 24 hours; one common observable on the host once the payload runs.

What happened on TanStack's CI side — the template the worm now replays per maintainer

The maintainer postmortem and the published incident analyses (Snyk, StepSecurity, Socket, Endor Labs, Enclave, the official advisory) all describe the same three-stage chain. It matters because the worm replays this template every time it reaches a victim maintainer next.

  1. "Pwn request" via pull_request_target. A fork PR (#7378, "WIP: simplify history build") triggered a workflow that checked out and executed fork-controlled code in the base repo's context — the textbook pattern GitHub Security Lab has been warning about for years. After the malicious build ran, the PR was force-pushed back to main's HEAD so the diff looked innocuous, then closed and deleted.

  2. Cache poisoning. The build wrote a poisoned pnpm store at Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11, scoped to refs/heads/main, ready to be served back to the next legitimate release run.

  3. OIDC token theft from Runner.Worker memory. When the next push to main kicked off the release workflow at 19:15 UTC, the poisoned cache was restored, attacker-controlled binaries scanned /proc/*/cmdline for Runner.Worker, read /proc/<pid>/maps and /proc/<pid>/mem, regex-extracted the OIDC JWT, and POSTed it to registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/<pkg> to mint per-package publish tokens. Five minutes later 84 malicious tarballs hit the registry. The release run reported "failed" — but the publishes had already happened from a different code path inside the same run.

No long-lived token. No social engineering. No modification of the publish step. Just untrusted PR code allowed to influence a cache that an OIDC-capable workflow later restored. The chain of trust held; the workflow execution path did not.

What the install-time payload does on every consumer

Inside the malicious tarballs, two static markers stand out: a git-based optionalDependencies entry pointing at orphan commit github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c, and a 2.3 MB router_init.js dropped at package root not declared in the package's files array. Once npm install resolves the git dep, the tooling installs Bun and runs the obfuscated init payload, which daemonises behind a _DAEMONIZED guard and proceeds with the textbook Shai-Hulud sequence:

  • Broad credential harvest. One process, one polling window: ~/.npmrc, ~/.git-credentials, every key under ~/.ssh/, ~/.aws/credentials, ~/.config/gcloud/application_default_credentials.json, ~/.azure/, ~/.kube/config, ~/.vault-token, ~/.docker/config.json, GitHub CLI configuration, and ~/.claude/projects/*.jsonl.

  • Cloud metadata theft. AWS IMDSv2 at 169.254.169.254, ECS at 169.254.170.2, GCP metadata, Vault at 127.0.0.1:8200 or vault.svc.cluster.local:8200.

  • Exfiltration over the Session/Oxen network. RSA-4096-OAEP-wrapped AES-256-GCM, ~100 KB chunks to filev2.getsession.org. Decentralised by design — fixed-IP blocking is unreliable, DNS-level blocking of *.getsession.org is the practical perimeter control. A secondary GitHub GraphQL dead-drop pushes blobs to attacker-controlled repos under dependabot/github_actions/format/{dune-word} branches; workflow-injection variants also POST to api.masscan.cloud.

  • Persistence. Drops ~/.claude/router_runtime.js, ~/.claude/setup.mjs, ~/.claude/settings.json, plus .vscode/setup.mjs and a .vscode/tasks.json that runs on folder open. Installs a gh-token-monitor service as either ~/.config/systemd/user/gh-token-monitor.service on Linux or ~/Library/LaunchAgents/com.user.gh-token-monitor.plist on macOS, plus an injected .github/workflows/codeql_analysis.yml in any reachable repo.

  • Dead-man's switch. The persistence service polls api.github.com/user every 60 seconds with the stolen GitHub token. On HTTP 40x, the public analyses report it runs rm -rf ~/. Disable persistence before rotating tokens, not after. An npm-token description string in the IOC set, IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner, removes any ambiguity about intent.

What EDAMAME catches today, deterministically

EDAMAME Security (desktop), EDAMAME Posture (CLI), and the edamame_posture_action (CI) share the same engine. Three checks fire on the consumer host without any prior knowledge of the package, payload, or campaign infrastructure:

  • credential_harvest (CRITICAL, anomaly-independent). The smoking gun. Fires when one process holds open files spanning three or more sensitive-path categories from the cloud-updated, signature-verified threatmodels database. The TanStack payload trips at least six categories at once in one bun / node process: git, ssh, aws, gcp, kube, vault. CRITICAL is a hard guardrail — the LLM post-filter is forbidden by invariant from suppressing it.

  • token_exfiltration (HIGH). Same process, plus an outbound external session that is either scored anomalous by flodbadd's 12-dimensional Extended Isolation Forest, or hits the deterministic branch (recent, external, sustained egress, non-routine destination). Both branches fire on the Session/Oxen filev2.getsession.org endpoint and on the litter.catbox.moe / api.masscan.cloud URLs.

  • file_system_tampering (CRITICAL). EDAMAME's filesystem integrity monitor watches ~/.claude/ by default. Every drop of router_runtime.js, setup.mjs, or modification of settings.json by a writer that is not the legitimate Claude Code app trips a CRITICAL finding.

In a 60-second tick after the install completes, an operator on a packet-capture-enabled host sees, at minimum:

CRITICAL credential_harvest bun <PID> -- 6 label categories
HIGH token_exfiltration bun <PID> -> filev2.getsession.org
CRITICAL file_system_tampering node <PID> ~/.claude/router_runtime.js
CRITICAL file_system_tampering node <PID> ~/.claude/setup.mjs

On a CI runner with the standard self-hosted-runner protection block enabled, the trailing exit_on_vulnerability_findings: true step aborts the job before any downstream signing or publish step can run.

What EDAMAME does not catch yet — and why we are saying so directly

Three stages of this campaign land outside the current shipping detector. We are putting them in the body of the post rather than burying them, because the credibility of runtime detection depends on operators knowing where the line is.

  • OIDC token theft from Runner.Worker memory. A non-cloud-init process reading /proc/<other_pid>/maps and /proc/<other_pid>/mem is not flagged today; the follow-on POST to registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/<pkg> is to a routine destination. This is the genuinely novel CI-side stage; filed in our backlog as BS-9. The natural regression trigger to land alongside the fix is trigger_process_memory_scrape.py.

  • In-process cloud-metadata theft when nothing lands on disk. A curl 169.254.169.254 piped straight into the exfil channel is not corroborated by a sensitive-file open. Filed as BS-10.

  • Persistence sinks outside the default FIM watch roots. LaunchAgents under ~/Library/LaunchAgents/, ~/.local/bin/ scripts, .vscode/ in arbitrary projects, and injected .github/workflows/* in non-CI mode all need wider FIM coverage. Filed as BS-11. This is why the operator runbook below is explicit about manual inspection.

Operator response — the order matters

  1. Read the alertable findings. edamame_cli rpc get_vulnerability_findings --pretty. The credential_harvest CRITICAL is the authoritative inventory of what was opened, with each path mapped to its label category in open_files.

  2. Disable the dead-man's switch BEFORE rotating any tokens. Linux: systemctl --user disable --now gh-token-monitor.service, then remove ~/.config/systemd/user/gh-token-monitor.service. macOS: launchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plist, then remove the plist. Also remove ~/.claude/router_runtime.js, ~/.claude/setup.mjs, any unexpected .vscode/tasks.json, and any injected .github/workflows/codeql_analysis.yml. Until BS-11 ships, this is a manual sweep.

  3. Rotate every credential listed under credential_harvest.open_files. Cloud first (aws, gcp, kube, vault), developer second (git, ssh, npmrc), agent state last (claude). Re-establish OIDC federation only after CI/CD hardening is complete.

  4. Block exfil channels at DNS: *.getsession.org, api.masscan.cloud, git-tanstack.com, the published litter.catbox.moe URLs.

  5. Rebuild from a clean image for any developer machine or runner that installed an affected version of @tanstack/*, @mistralai/mistralai*, @uipath/*, @opensearch-project/opensearch, the Bitwarden CLI, the affected SAP packages, or any other entry in CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx after 19:20 UTC on 11 May 2026. Lock-step note: @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store, and the @tanstack/start meta-package are clean per the maintainer postmortem.

CI/CD hardening that would have broken the chain

  • Stop running untrusted PR code in privileged contexts. Combining pull_request_target with explicit checkout of fork-controlled code is the textbook "pwn request"; release workflows must not restore caches written by jobs that execute untrusted PR code.

  • Scope id-token: write to the publish job, not the workflow. A lazily minted OIDC token can only be scraped from memory if it has been minted.

  • Pin trusted-publisher relationships to a specific workflow file and protected branch, not the repository. Use ephemeral runners for release jobs. Eliminate or segregate dependency caches in release pipelines.

  • Enforce --ignore-scripts and --allow-git=none during triage windows and high-risk install paths. GitHub introduced --allow-git=none in February 2026 specifically because git dependencies still lead to arbitrary code execution paths even when scripts are otherwise suppressed.

  • Run the EDAMAME runtime detector on the runner. The CI-side novel stage will be closed in detector terms, and the consumer-side install payload already trips three CRITICAL/HIGH findings before any downstream publish step can run.

Reproduce the install-time half safely

The Shai-Hulud / TanStack family is a behavioural superset of the install-time payloads we already exercise. Until the dedicated trigger_tanstack_shaihulud.py scenario lands, the closest existing analog with the same credential_harvest + token_exfiltration + file_system_tampering profile is the pgserve trigger:

python3 trigger_pgserve_postinstall.py --agent-type openclaw --interval 0.5 --duration 600

Install EDAMAME Posture (free CLI) or EDAMAME Security (free desktop app), start the daemon with packet capture enabled, run the trigger, wait one detector cycle, then read the findings. Real campaign IOC hosts are hard-blocked by the trigger's validate_target; traffic goes to a neutralised lab target. The trigger sits alongside trigger_supply_chain_exfil.py (LiteLLM), trigger_npm_rat_beacon.py (axios), trigger_credential_sprawl.py, and six other CVE-aligned scenarios.

Provenance proves origin, not behaviour

Every supply-chain compromise we have written about this spring has reinforced the same point in a different ecosystem. Trivy was credentials. LiteLLM was credentials. axios was credentials. pgserve was credentials. TanStack — and Mistral, UiPath, OpenSearch, Bitwarden, SAP, Guardrails, and the 160-plus other projects the worm reached in 24 hours — are the first wave where credentials were never the problem on any of them. Each affected publisher's OIDC trust was scoped correctly, each one's provenance attestations verified, each was running the configuration the ecosystem has been recommending for two years. The publishes were malicious anyway, because the worm carried its own SLSA-attestation forgery routine and turned every compromised maintainer's pipeline into another fully-attestable distribution channel.

SBOMs, hash verification, signature checks, and provenance attestations are necessary controls. They protect against tampering in transit, and they answer "did this artifact come from the workflow it claims to come from?" They cannot answer "was the workflow doing the right thing when it ran?" — and after this campaign, they cannot reliably answer "did the attestations come from the publisher's own infrastructure?" either, because the worm now ships its own keypairs. That is only ever answerable from the host, by watching what the resulting code does on a real machine. That is the durable detection strategy and it is what runtime behavioural detection is for.

Five compromises in eight weeks; one with 169+ named victim projects on day one; one with a destructive payload now operational in the wild. Install detection now — while the window is open and the threat is visible. Not after the next incident. Not after your runner.

Get started

Sources: TanStack maintainer postmortem (11 May 2026), CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx, Snyk (169 / 373 count), Endor Labs (160+ count and orphan-commit analysis), Socket (up to 416 artifacts), StepSecurity (initial detection and malware report), Enclave (SLSA Build Level 3 architectural analysis), Microsoft Threat Intelligence (PyPI transformers.pyz destructive payload and geofencing), GitHub Security Lab "pwn-request" guidance, Adnan Khan's GitHub Actions cache poisoning research, and EDAMAME's prior coverage: LiteLLM, axios, pgserve / CanisterSprawl.

Minh Anh Day

Share this post