tech, developers, and the code underneath

issue 071· news·

Shai-Hulud: npm gets a self-replicating worm

A payload that steals credentials and then uses them to publish itself into other packages. This is the escalation everyone predicted.

A self-propagating worm spread through npm this week, compromising hundreds of packages. The mechanism is the escalation people have been warning about for years, and it arrived exactly as described.

how it works#

The loop:

  1. A malicious package version is installed. Its postinstall runs.
  2. The payload harvests credentials from the machine — npm tokens, GitHub tokens, cloud provider credentials — using TruffleHog-style secret scanning.
  3. It exfiltrates them to a public repository created in the victim's GitHub account.
  4. It uses the stolen npm token to publish trojanized versions of every package that token can publish to.
  5. Those packages get installed. Go to 1.

Step four is the difference between an incident and an outbreak. Previous npm compromises required an attacker to manually obtain credentials for each package. This one propagates on its own, and its rate of spread is proportional to how many packages the compromised maintainers control.

Additional behavior observed: creating public forks of private repositories in victims' organizations, and installing a GitHub Actions workflow for persistence.

why this was inevitable#

Every precondition has been in place for years:

  • Packages execute arbitrary code on install, by default.
  • Publishing credentials are commonly stored on developer machines in plaintext.
  • One credential frequently controls many packages.
  • Nothing in the publishing flow requires human presence.

Given those four facts, a worm is not a clever attack. It is the obvious one. Security researchers have described this exact scenario in talks for the better part of a decade.

the immediate response#

npm has accelerated changes it had already announced: shorter token lifetimes, mandatory two-factor for high-impact publishing, restrictions on classic tokens, and a strong push toward trusted publishing.

Those are the right changes. They are also the changes that were "coming" for years and are now arriving under emergency conditions, which is how infrastructure security usually improves.

what to do right now#

Rotate every npm token you have, especially any on a developer machine or in a CI secret store. Assume anything that was on a machine that ran an install during the window is exposed.

Enable trusted publishing on every package you maintain. This removes the long-lived token entirely — publishing is authorized by an OIDC assertion from a specific workflow in a specific repository.

Audit your GitHub account for repositories and workflows you did not create. The persistence mechanism was a workflow file; it survives credential rotation.

Turn off install scripts everywhere you can. This remains the single highest value control and remains widely unused.

Adopt a version cooldown. Every one of these incidents has a window between publication and detection measured in hours. A 72-hour delay before adopting new versions costs you nothing and avoids nearly all of them.

the structural conclusion#

The npm ecosystem's install-time code execution is a design decision from 2010 that made sense when the registry had a few thousand packages maintained by people who mostly knew each other.

It does not make sense now, and every incident makes the argument more forcefully. Other ecosystems handle this differently — Go has no install-time execution at all, and it turns out you can build a package ecosystem without it.

The migration cost for npm is enormous and the cost of not migrating is this, repeatedly, with escalating sophistication. At some point the arithmetic flips. It may have just flipped.

Dom, September 16, 2025

get README in your inbox

One dispatch, no noise. Tech and developer news, plus the occasional long piece on the craft.

subscribe →