shai-hulud: the npm worm that propagates through postinstall scripts
"Shai-Hulud" is a name researchers have used for a class of self-replicating npm worms whose payloads include code that scans the infected developer's environment for npm publishing credentials and uses them to publish malicious versions of other packages the developer maintains. Where most supply-chain attacks compromise one package, a worm that finds publish tokens compromises every package that token can publish to.
This post explains the propagation mechanism and what reduces the blast radius.
How the propagation works
The worm class follows a simple pattern.
- A malicious version of some package is installed on a developer machine, typically via a postinstall lifecycle script that runs at install time.
- The script searches the host for credentials:
~/.npmrcand per-project.npmrcfiles containing auth tokens; environment variables such asNPM_TOKEN; CI configuration in well-known paths. - For each token it finds, it queries the npm registry for packages that token can publish to.
- For each such package, it downloads the latest version, modifies it to include the same propagation script, bumps the patch version, and publishes.
- The new malicious versions then sit on npm, waiting for their next install on another developer's machine, where the cycle repeats.
The exact set of credential locations and the exfiltration pattern vary across observed variants. The structure is consistent.
Why worms are different from one-off compromises
A one-off compromise affects users who install the affected package versions during the exposure window. A worm with credential-scraping propagation affects users who install any package that any compromised credential can reach. Each successful infection multiplies the attack surface.
The worst cases observed have produced compromised versions of dozens of unrelated packages within hours of the initial compromise, because the first developer it lands on may maintain or have publish access to many packages.
What makes the attack effective
Postinstall scripts run by default. When you npm install a package, its postinstall (and preinstall, install) scripts run with your shell privileges. Disabling lifecycle scripts via npm config set ignore-scripts true blocks this entirely but breaks legitimate packages that need a build step.
Authoring tokens are broad. A standard npm publish token can publish any package the user has rights to. There is no per-package scoping by default.
Token storage is convenient. ~/.npmrc exists exactly so that npm publish works without prompting. That convenience makes the token easy to find.
What reduces the blast radius
Granular tokens. npm supports access tokens scoped to specific packages with limited permissions. A CI token that can only publish one package, only from one IP range, makes the credential nearly useless to a worm.
No publishing from developer machines. Move package publishing entirely to CI. A developer machine should not have a publish-capable token at all. CI tokens live in CI secret stores, not in ~/.npmrc.
Trusted Publishing. npm and PyPI both support OIDC-based publishing where the registry authenticates a CI workflow without long-lived tokens. There is no token for a worm to find. We cover this in detail in the Trusted Publishing post.
--ignore-scripts for installs that don't need lifecycle hooks. For most application dependencies, lifecycle scripts are not needed at install time. Audit which packages actually require them and disable scripts globally otherwise.
Cooling-period gates. A worm-pushed version is, by definition, brand new on the registry. A cooling gate refuses to install brand-new versions automatically, which prevents the second-hop propagation: a developer who installs a freshly compromised version is the link that turns a one-host infection into a multi-package compromise. Hold that link and the worm cannot spread.
What to do if you are compromised
Rotate every npm token associated with any account whose credentials may have been on the compromised machine. Revoke tokens via the npm web UI; do not rely on local removal alone. Audit the package list each token had publish rights to and check the most recent versions for unexpected changes. Consider all CI runs from compromised tokens as suspect for the period in question.
Takeaway
Self-replicating npm worms are the worst-case shape of npm supply-chain risk because compromise compounds. The defenses that matter are about removing both the propagation surface — broad tokens, postinstall scripts that run by default — and the second-hop install path that lets a freshly compromised version land on the next host before anyone has noticed.