How 84 npm Packages Were Poisoned via a Legitimate CI/CD Pipeline

On May 11, 2024, attackers injected 84 malicious versions across 42 @tanstack packages into the npm registry, all bearing valid SLSA Level 3 signatures, by hijacking TanStack's CI/CD workflow through a Pwn Request, cache poisoning, OIDC token extraction, and rapid release, exposing a critical supply‑chain vulnerability.

Black & White Path
Black & White Path
Black & White Path
How 84 npm Packages Were Poisoned via a Legitimate CI/CD Pipeline

Event Overview

On May 12 (UTC 19:20), 84 malicious npm package versions appeared covering 42 @tanstack/* packages. The releases were signed with valid SLSA Level 3 signatures, making them indistinguishable from legitimate packages. The attack chain consisted of a Pwn Request → cache poisoning → OIDC token extraction → direct publishing. Socket Security flagged all malicious versions within six minutes, but npm's unpublish restriction delayed removal for several hours.

42 packages , 84 malicious versions (two per package, released in two batches six minutes apart)

169+ packages indirectly affected, including @mistralai, @uipath, @squawk, etc.

127 k weekly downloads for @tanstack/react-router, the most impacted package

20 minutes for external researcher Carlini to publish a full technical report

6 minutes for Socket to mark all malicious versions

Attack Timeline: A Precise CI/CD Hijack Route

Step 1 – Preparation (May 10 – May 11 morning)

The attacker created a GitHub account zblgg (ID 127806521), forked the TanStack/router repository, renamed it to zblgg/configuration to avoid detection, and submitted a forged commit impersonating [email protected]. The commit injected ~30 k lines of bundled JavaScript and added the [skip ci] flag to bypass CI triggers.

At 10:49 UTC on May 11, the attacker opened PR #7378 titled “WIP: simplify history build”. The PR triggered the bundle-size.yml workflow, which used the pull_request_target event that checks out and runs code from the forked repository.

This pattern matches the “Pwn Request” technique described by security researcher Adnan Khan in May 2024.

Step 2 – Cache Poisoning (May 11 11:01‑11:29 UTC)

The forked code wrote a malicious pnpm store entry that precisely matched the cache key logic of production release.yml ( Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')}). By 11:29 UTC, 1.1 GB of poisoned cache data was written and remained dormant for about eight hours.

The workflow attempted permission separation by annotating the benchmark job as “read‑only”, but the actions/cache@v5 step used the runner’s internal token ( permissions: contents: read), rendering the restriction ineffective.

Step 3 – Publishing Malicious Versions (May 11 19:15‑19:26 UTC)

A legitimate PR merge triggered release.yml. The restored cache caused the attacker‑controlled executable to run, extracting an OIDC token from the GitHub Actions runner memory and publishing directly to the npm registry, bypassing the intended release‑approval step.

Both batches of 84 packages were published within six minutes, each signed with a valid OIDC identity and SLSA proof. The release.yml reported a status of “failure”, indicating the publish occurred outside the normal release flow.

Affected Packages Overview

@tanstack ecosystem (42 packages, 84 versions)

The most impacted package is @tanstack/react-router (127 k+ weekly downloads). Affected versions include 1.169.5/1.169.8 (Router series) and 1.167.68/1.167.71 (Start series). All core routing packages were compromised ( @tanstack/vue-router, @tanstack/solid-router, @tanstack/router-core, etc.), while query, table, form, virtual, and store packages remained clean.

Secondary Victims (Self‑Propagation Mechanism)

The payload enumerated all packages within the victim’s npm scope and republished the injected versions using the same OIDC credentials, spreading to:

@mistralai : TypeScript client versions 2.2.2‑2.2.4

@uipath : Over 40 automation platform packages

@squawk : 19 aviation data packages

Other scattered packages such as safe-action, cmux-agent-mcp, nextmove-mcp At least 170 + npm packages have been flagged in security vendor databases.

Malicious Payload Deep Dive: router_init.js

Each compromised tarball contained a hidden 2.3 MB obfuscated JavaScript file router_init.js that is not listed in the files field of package.json, indicating injection outside the build process. Any integrity check of the tarball can immediately detect the anomaly.

Three‑Layer Obfuscation

Layer 1 : Standard JavaScript obfuscator – string array rotation with a self‑invoking function; the distribution function _0x253b is called 2 864 times, replacing all string literals with array lookups.

Layer 2 : Byte‑wise Fisher‑Yates substitution cipher using a hard‑coded master key derived via PBKDF2‑SHA256 (200 k iterations). Decryption reveals a C2 domain, credential path, and internal partition name “EveryBoiWeBuildIsAWormyBoi”.

Layer 3 : Eleven AES‑256‑GCM payloads compressed with gzip, requiring the Bun runtime for decryption. Unit 42 identified the same PRNG seed in SAP, Bitwarden, and this TanStack attack, proving a shared code base across three attack waves.

Credential Harvesting List

AWS: IMDSv2 metadata service, Secrets Manager

GCP: metadata endpoint

Azure: service‑principal credentials

Kubernetes: Service Account Token

HashiCorp Vault: local token cache

GitHub: environment variables, gh CLI, .git-credentials npm: ~/.npmrc auth token

SSH: ~/.ssh/ private keys

Three C2 Channels

Typosquat domain git‑tanstack.com (looks like tanstack.com)

Session P2P network *.getsession.org with end‑to‑end encryption, resistant to IP/domain blocking

GitHub API backdoor: stolen token used to create a repo titled “Shai‑Hulud: Here We Go Again”

Persistence and Destruction Mechanism

After obtaining a valid GitHub token ( ghp_ / gho_), the payload installs a gh-token-monitor daemon (macOS LaunchAgent or Linux systemd) that runs detached from any terminal. It polls GitHub every 60 seconds; if the token is revoked, the daemon executes rm -rf ~/ after a 24‑hour grace period – a classic retaliatory wiper. The payload also checks for a Russian locale and exits immediately without exfiltrating data, showing targeted regional evasion.

Developer Self‑Check Guide

Level 1 – Inspect Installation Records

Search package-lock.json, pnpm-lock.yaml, or yarn.lock for the following versions:

@tanstack/*: 1.161.9, 1.161.12, 1.166.x, 1.167.x, 1.168.x, 1.169.x
@mistralai/mistralai: 2.2.2, 2.2.3, 2.2.4

Quick global scan commands:

# npm
npm ls @tanstack/react-router @tanstack/router-core @tanstack/history

# pnpm
pnpm list @tanstack/*

# yarn
yarn list @tanstack/*

Level 2 – Locate Malicious Files

# Find router_init.js
find . -name "router_init.js" 2>/dev/null

# Find suspicious setup.mjs in node_modules
find . -path "*/node_modules/*/setup.mjs" 2>/dev/null

Level 3 – Check Persistent Daemons

# Linux systemd
ls ~/.config/systemd/user/gh-token-monitor.service 2>/dev/null

# macOS LaunchAgent
ls ~/Library/LaunchAgents/com.user.gh-token-monitor.plist 2>/dev/null

Level 4 – Rotate Credentials

If you installed an affected version before 2026‑05‑11, immediately rotate the following credentials:

GitHub Token (PAT/OAuth ghp_/gho_)

npm Token ( ~/.npmrc authToken)

AWS IAM role credentials (IMDS, Secrets Manager)

GCP service‑account credentials (metadata server)

Kubernetes Service Account Token

HashiCorp Vault local token

SSH private keys ( ~/.ssh/)

Level 5 – Block C2 Domains/IPs

At the firewall or proxy layer, block:

git-tanstack.com
*.getsession.org
83.142.209.194

Postmortem and Lessons

The Irony of the Attack

The npm registry itself was never breached, nor were npm credentials stolen. The OIDC token used for publishing was a legitimate token issued by TanStack, and the SLSA signature passed verification. The attack exploited a dismantled trust chain rather than a credential leak.

Six Issues Worth Recording

Missing internal alerts. TanStack learned of the breach from third parties; internal monitoring of publishing activity was absent.

Unaudited pull_request_target . This high‑risk pattern has been highlighted in research since 2024 and remains common in open‑source projects.

Floating third‑party Action references. Using @v6.0.2 or @main allows upstream changes to flow into the pipeline; pinning to a commit SHA is the only safe remedy.

npm’s irrevocable publish policy. The “cannot unpublish when dependents exist” rule delayed removal for over ten hours, keeping the malicious tarballs installable.

OIDC‑based trusted publishing lacks source verification. Any code path in the workflow can publish once the token is bound; adding source verification or short‑lived classic tokens with manual review mitigates this.

SLSA’s blind spot. SLSA validates the build process, not the safety of the built code. This attack demonstrated the first real‑world case where a malicious package carried a valid SLSA Level 3 signature.

In total, 84 packages were released in two batches over six minutes. Developers who ran npm install @tanstack/[email protected] during the 20‑minute window downloaded a legitimately signed malicious package. The incident underscores that supply‑chain attacks stem from broken trust assumptions rather than pure technical flaws.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CI/CDSupply ChainSecuritynpmSLSAOIDC
Black & White Path
Written by

Black & White Path

We are the beacon of the cyber world, a stepping stone on the road to security.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.