flawopen.com/Incidents/Shai-Hulud npm worm
Imagine a factory where any worker can ship a new batch of parts, and every downstream factory installs those parts automatically without inspecting them. Someone tricks one worker into handing over their badge. The thief doesn't just tamper with one batch — they hide a device in it that, when the box is opened at the next factory, steals that factory's badges and uses them to ship tampered batches onward. Nobody is driving it any more. It spreads on its own.
In mid-September 2025 the npm registry was hit by a self-replicating supply chain worm that researchers named Shai-Hulud, after the sandworms of Dune. It compromised hundreds of packages — reporting at the time put the figure in the region of 500 — and it did so with no human operator directing each step.
The entry point was mundane: a phishing email dressed up as an npm security notice, which harvested a package maintainer's credentials. What made the incident significant was everything that happened after that first account fell.
This was a distinct event from the npm compromise roughly a week earlier, in which a separate maintainer was phished and widely-depended-upon packages were trojanised with cryptocurrency-stealing code. The two are often conflated because they occurred in the same month and both began with maintainer phishing.
The worm's design is the whole story. Each infected install performed four steps, the last of which produced the next generation:
A malicious lifecycle script ran automatically when a developer or CI runner installed the package. Nothing needed to require() the package; merely installing it was sufficient.
The payload bundled a legitimate open-source secret-scanning tool and pointed it at the machine — hunting npm tokens, GitHub tokens, and cloud provider credentials from environment variables, config files and the CI environment. Build machines are unusually rich targets here: they hold the credentials for everything the project publishes and deploys.
Rather than calling out to an obvious command-and-control server, the worm pushed stolen data into attacker-visible locations on platforms the victim already used — including publicly-created repositories on the victim's own GitHub account and injected GitHub Actions workflows. Traffic to github.com from a build server is not anomalous, which is precisely the point.
With a harvested npm token, the worm enumerated the packages that victim maintained and published trojanised versions of them carrying the same payload. Every developer installing those packages became step 1 of a fresh cycle.
That fourth step is what separates this from a conventional malicious-package incident. The attacker did not need to compromise hundreds of maintainers. They needed to compromise one, and let the loop find the rest.
A dependency you never call still runs code on your machine at install time. That is the design decision the whole attack rests on.
// package.json of a trojanised release { "name": "some-popular-util", "version": "4.1.3", "scripts": { // runs automatically on install, // with your shell privileges "preinstall": "node bundle.js" } }
# Install without running any # lifecycle scripts: npm ci --ignore-scripts # Or make it the default for the # project / CI environment: npm config set ignore-scripts true # Note: some packages with native # builds genuinely need scripts. # Allowlist those deliberately.
npm ci --ignore-scripts neutralises the execution primitive this worm depended on. Allowlist the small number of dependencies that truly need a native build step.npm ci, not npm install. A lockfile means a freshly published malicious version is not silently pulled into your build the hour it appears.Most dependency-security tooling is built to answer "does this package have a known CVE?". A freshly trojanised version of a package you already trust has no CVE, a legitimate history, and a maintainer in good standing. Shai-Hulud is the argument for treating installation itself as the security boundary — controlling what executes, what credentials are within reach when it does, and how quickly new versions are adopted — rather than relying on after-the-fact vulnerability lists.
Check whether any affected package version appears in your lockfiles or CI logs for the exposure window, look for unexpected public repositories or new Actions workflows on developer and organisation GitHub accounts, and audit npm and GitHub token creation and package publication events. If a build machine ran an affected version, treat every credential that machine could reach as compromised and rotate it.
--ignore-scripts fully protect me?It removes the install-time execution path, which is what this worm used. It does not protect you from malicious code inside a package you actually import and run. It is a strong control, not a complete one.
No. Further variants using the same self-propagating pattern were reported in late 2025 and into 2026, and the technique has since been observed spanning multiple registries. The structural weaknesses it exploits — automatic script execution and long-lived publish tokens — are properties of the ecosystem rather than of any one package.