Shai-Hulud: What an NPM supply-chain hack reveals about the limits of provenance

polynomial1 pts0 comments

The Latest Software Supply Chain Hack (and what to do about it)

SubscribeSign in

The Latest Software Supply Chain Hack (and what to do about it)<br>Oh look npm registry got compromised… again.

CTO Lunch NYC<br>Aug 05, 2026

Share

A moreorless highly technical analysis of the latest hack on the npm registry<br>📌 skip to 30/60/90

If you are currently sitting in a hotel lobby in Las Vegas for Hacker Summer Camp, 1 your phone has probably spent the last four hours buzzing itself off the table. 2<br>While half the industry is listening to briefings on LLM jailbreaks and hardware side-channels, an automated supply chain wurm (a variant of the Mini Shai-Hulud3 payload associated with TeamPCP) was quietly ripping through the Node.js ecosystem at a rate of roughly a hundred downstream packages an hour. The estimate is that this attack is the #3 worst incident in npm’s long and storied history of such problems, following Left-Pad at #2 and 2018’s Event Stream holding down #1.4<br>Supercritical Scale Up

If you cloned the keyv repository after 09:00 UTC yesterday (4 Aug) even to audit the compromise—or any library that transitively depends on it, obvi— your IDE may have executed the payload.

The cascade began in the quiet, unglamorous bedrock of the Node.js ecosystem : attackers compromised the GitHub account of the primary maintainer behind keyv, (a staple key-value DB interface with ~127M weekly downloads) and, because dependency graphs in the modern JavaScript ecosystem resemble dense, entangled bamboo root systems more than clean directed acyclic graphs, compromising keyv instantly granted access to its sister caching layer abstractions, cacheable, flat-cache, & file-entry-cache as well, all low-level primitives sitting near the root of thousands of transitive dependency graphs (but mostly bc Jared also controlled these ubiquitous caching libraries):<br>keyv

cacheable

flat-cache

file-entry-cache

From there, the wurm executed a self-propagating loop directly inside the build environment; within a 4-hour window, the wurm harvested tokens from build runners and compromised over 440 to 860 downstream packages across other maintainers, affecting libraries with a combined footprint of over 2 billion monthly downloads.<br>The resulting blast radius was the direct, mathematical output of<br>how npm’s maintainer-trust model composes at graph scale.

SafeDep enumerated 1,684 poisoned versions across 420 package names against the registry by early afternoon UTC. SafeDep’s real-time tracking put the count at 868 packages across 1,381 versions by 13:37 CEST. The wurm reached nine unrelated organizations in approximately thirty minutes, including @deliveroo, @qlik, @servicetitan, @ornikar, @adminide-stack, and @arv-bedrock, moving from one namespace to the next every two to seven minutes and republishing at roughly one package per second.

When the maintainer’s credential path was compromised, the resulting blast radius wasn’t owing to the attacker’s extraordinary sophistication (sorry guys) but the direct, mathematical output of how npm’s maintainer-trust model composes at graph scale. If you inspect the raw package diffs, there are no zero-day kernel exploits, no memory corruption primitives, no tricky buffer overflows. Instead, the attack relies on the oldest execution primitive in the ecosystem: the humble preinstall hook.<br>The published package.json for keyv@6.0.05 looks, to a casual audit, essentially like the previous release: dist/ output is byte-identical to the last clean version. (!)<br>The attack surface is two added files and a single modified field:<br>"files": ["dist", "LICENSE", "setup.mjs", "Math_Symbol.js"],<br>"scripts": {<br>"preinstall": "node setup.mjs"<br>}setup.mjs is the first-stage loader. It is lightly obfuscated Node that detects platform and architecture (including Alpine/musl variants, via ldd --version and /etc/os-release), fetches a platform-matched standalone Bun runtime at version 1.3.13 if one is not already present, unzips it using system unzip on POSIX, PowerShell Expand-Archive on Windows, or a hand-written pure-JavaScript ZIP parser as a fallback, and then executes the second stage under the freshly fetched binary:<br>const V = "1.3.13";<br>const E = "math_init.js";<br>const url = "https://github.com/oven-sh/bun/releases/download/bun-v" + V + "/" + target + ".zip";<br>// ...<br>execFileSync(bunBinary, [payloadPath], { stdio: "inherit", cwd: D });Math_Symbol.js is the second stage , approximately 728 KB bundled. Wiz attributes the payload lineage to the “Mini” Shai-Hulud malware family, the same codebase that powered the TeamPCP campaign against Mistral, PyPi and TanStack packages back in May6 and the Red Hat Cloud Services OIDC-bypass incident in June. The Shai-Hulud open-source repositories that TeamPCP published form the ancestor; this variant diverges on several specifics.<br>Running the second stage under a downloaded Bun binary sidesteps the host Node version and any Node-level process monitoring. The IOC for this is the process...

node keyv shai supply chain hack

Related Articles