WhatsAppDamascusSat – Thu·10:00 AM – 7:00 PM

npm install runs code you have never read

Eighteen malicious versions of one package went out in two and a half hours, harvesting tokens on install. Lockfiles, disabled scripts and short-lived credentials.

SecurityPublished 3 min read

Two and a half hours

On 11 May 2026, between 20:19 and 22:56 UTC, someone with a stolen npm publish token pushed eighteen versions of @beproduct/nestjs-auth0.1.2 through 0.1.19. They carried a payload from the Mini Shai-Hulud supply-chain worm. It is tracked as CVE-2026-46412, CVSS 10.0, and it has no patched version, because there is no fix for a package that was malicious on purpose. npm removed it.

The payload ran on install and went looking for credentials: npm tokens, GitHub personal access tokens, OAuth tokens, and the OIDC tokens that CI jobs use to authenticate to cloud providers. Then it used them to publish itself further. That is what makes it a worm rather than a theft — every developer who installed it became a publisher of it.

Nobody had to run the application. npm install was enough.

What install actually does

A lifecycle script — preinstall, install, postinstall — is code from a stranger, executed by your shell, as your user, with your environment. Your environment contains NPM_TOKEN, GITHUB_TOKEN, ~/.ssh, ~/.aws, a .env file, and on CI an OIDC token that can mint cloud credentials.

We accept this a hundred times a day because the alternative sounds impractical. It is not.

The four things that actually help

Install with scripts disabled. For most dependency trees this simply works:

bash
npm ci --ignore-scripts

Some packages genuinely need a build step — native modules, mostly. Find them, list them, and allow those specifically rather than allowing everything. If you use pnpm, onlyBuiltDependencies expresses exactly this.

Never install from a floating range in CI. npm ci installs the lockfile. npm install may resolve a caret range to a version published nine minutes ago. The eighteen malicious versions above were all inside somebody's ^0.1.0.

Delay adoption. A cooldown — refusing to install any version published in the last few days — would have covered the entire window of this incident, and of most others like it. The bad versions were live for hours, not weeks.

Scope the tokens. A publish token that can push every package you own is a worse loss than one that can push one package. Prefer short-lived OIDC over long-lived secrets in CI, and keep publish tokens off developer laptops entirely.

The other half: your laptop is a target

CVE-2025-54782 in @nestjs/devtools-integration, patched in 0.2.1, is the same lesson from the other direction. The package ran a local HTTP server with an unsafe JavaScript sandbox and no cross-origin protection. Any website a developer visited while it was running could execute code on their machine.

A development server bound to localhost is not private. Every page in the browser can send requests to it. Treat dev tooling as something that must be off when you are not using it, and pin it out of production dependencies.

If you think you were hit

Assume every credential that existed in that environment is gone. Rotate them: npm tokens, GitHub tokens, SSH keys, cloud keys, anything in a .env on that machine. Then read your npm and GitHub audit logs for publishes and pushes you did not make — the theft is not the damage, the use of what was stolen is.

For a firm that deploys other people's projects, the exposure is not one laptop. It is every client whose deploy key sat on it.

The uncomfortable summary

You cannot read every dependency. Nobody does. What you can do is stop granting install-time code execution by default, stop resolving versions at build time, and hold credentials that expire. None of that requires trusting the ecosystem less than you already, quietly, should.

npm install runs code you have never read · Qasioun Cloud