On the night of 15 September 2025, malicious versions of @ctrl/tinycolor and dozens of other npm packages appeared on the registry. The code ran at install time, searched the machine for GitHub tokens and cloud API keys, and posted them to a public repository named "Shai-Hulud". Then it used any npm token it found to publish infected versions of the victim's own packages. CISA's alert a week later put the count at more than 500 compromised packages.

The incident is a useful reminder that a clean dependency scan yesterday says little about a compromised release tomorrow.

What made the worm work

The worm needed three things. It needed a developer or CI job to install a new version without anyone reading it. It needed install scripts to run automatically. And it needed tokens with publish rights or broad cloud access sitting on the machine. Remove any one and the chain breaks.

Our rules now

Exact versions, always. Every project commits its lockfile. CI installs with npm ci, which fails if package.json and the lockfile disagree. Nobody runs a bare npm install on a build server.

Updates are pull requests. Dependabot or Renovate opens the PR. A developer reads the changelog and, for packages with install scripts, the diff of what runs on install. We wait a few days before merging non-security updates. Most malicious releases on npm get pulled within hours, so a short delay costs little and removes a lot of risk.

Install scripts are off by default. We set ignore-scripts=true in the project's .npmrc and allow scripts only for the few packages that need them to build native code. This alone would have stopped this worm on our machines.

CI tokens can't publish. Build pipelines get read-only access to the registry. The few packages we publish go out through a separate job with its own token, trusted publishing where the registry supports it, and a human approval step.

No long-lived cloud keys on laptops. Developers sign in to cloud consoles with short-lived credentials through SSO. A stolen laptop token that expires in an hour is worth much less than a key that lives forever.

The morning-after checklist

If you maintain a JavaScript project and hear about a new supply chain attack, this is the order we work in:

  1. Search every lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock) and your artifact cache for the affected package versions.
  2. If you find a match, treat the machines that installed it as compromised. Rotate GitHub, npm and cloud credentials that were present there.
  3. Check your GitHub account for new public repositories or workflows you didn't create.
  4. Pin the affected packages to a version published before the incident and rebuild.

CISA's guidance for this incident followed the same lines: review lockfiles, pin to versions released before 16 September 2025 and rotate credentials.

The cost

These rules add friction. Updates take a little longer and new developers need a note about .npmrc. Measured against rotating every secret in a company because one npm install ran on the wrong laptop, it's a small price.

Sources: CISA alert, 23 September 2025, Kaspersky analysis.

← More in Security