DEV Community

Jason Miller
Jason Miller

Posted on Originally published at axeploit.com

npm audit Won't Save You in the First Hour of a Supply-Chain Alert

When the ua-parser-js maintainer's npm token was stolen in October 2021, the poisoned release sat live for about four hours. npm audit reported zero vulnerabilities during those four hours, for a package with 7 million weekly downloads. If your incident plan starts with "run the scanner," you don't have an incident plan.

Here's the hour after the alert fires: three questions, fixed order. Everything else waits.

Before you touch a terminal, write at the top of a shared doc the exact package names and version ranges, plus the exposure window with timestamps. Headlines blur scope. When two malicious packages impersonating Axios were caught, Axios itself was never backdoored, and teams that spent their first hour auditing a clean HTTP client wasted it. You can't triage "the npm thing." You can triage a list.

Q1: Is a bad version in any lockfile, present or past? (minutes 10 to 25)

Your package.json is lying to you. A semver range says what could install; the lockfile is the only record of what did install on a given Tuesday. Poisoned releases usually look like a routine patch bump, so start from the lockfile in every repo.

The step most teams skip: search history, not just HEAD. The malicious version may have been installed during the exposure window and replaced by a "fix" bump yesterday. A clean current lockfile doesn't clear you. Credential theft happened at install time, weeks ago.

# commits that touched the package's lockfile entry
git log --oneline -G'"node_modules/PACKAGE_NAME"' -- package-lock.json

# what was resolved during the advisory window
git show <commit>:package-lock.json | grep -A2 '"node_modules/PACKAGE_NAME"'
Enter fullscreen mode Exit fullscreen mode

Run npm ls PACKAGE_NAME too, because the malice often sits several levels deep and a grep of direct deps misses it. No hit anywhere, current or historical? Document the search and stand down. Any hit moves you to Q2.

Q2: Did it actually run? (minutes 25 to 45)

A version in a lockfile is exposure. A version that executed is a breach. Don't conflate them.

Payloads fire through install-time lifecycle scripts or at runtime on import. Runtime payloads don't care about your --ignore-scripts flag. Pull CI logs from inside the window, confirm which version resolved, and check whether the pipeline used npm ci --ignore-scripts. If it did and the payload is install-time, CI likely dodged it. Every developer who ran a plain npm install did not.

Egress is your only witness. Grep proxy or VPC flow logs for domains outside baseline. On a suspect machine:

find node_modules -name '.npmrc' -o -name 'setup_*.js'
grep -rEl "eval\(atob|Buffer\.from\(|process\.env\.(NPM_TOKEN|GITHUB_TOKEN)" node_modules/PACKAGE_NAME
Enter fullscreen mode Exit fullscreen mode

A .npmrc inside node_modules is a huge red flag. If the package executed anywhere, treat every secret reachable from that system as compromised. That's what TanStack's maintainers told their own users after their compromise.

Q3: Rotate in blast-radius order (minutes 45 to 60)

You can't rotate everything in fifteen minutes, so order by how fast a stolen secret converts to damage:

  1. Cloud and deploy credentials. Usable from any machine on the internet within seconds.
  2. npm automation tokens. Self-replicating worms like the Shai-Hulud variant turn a stolen publish token into the next advisory your peers are triaging.
  3. Source-control tokens. A VCS token lets an attacker rewrite your workflows and wait.
  4. Developer-machine material. SSH keys, personal .npmrc tokens. Slow doesn't mean safe.

If egress logs prove nothing left the box, you can descope. Most teams discover right here that they have no egress logs at all. Absence of evidence isn't evidence. When in doubt, rotate.

"It's only a devDependency" inverts the threat. The pipeline is the target. devDependencies execute in exactly the two places your most powerful credentials live: CI runners and developer machines. Production is what attackers reach through those systems. Same answer for "we pin exact versions": good, keep doing it, but pinning freezes the graph going forward and says nothing about what you resolved during the window. That's why the history search exists.

Do today:

  • Run the git log -G search for your top ten dependencies so the command is muscle memory before you need it
  • Switch CI to npm ci (never npm install) and add --ignore-scripts where the build tolerates it
  • Turn on whatever egress logging your CI provider offers, even in preview
  • Write the three-question order into your incident doc so nobody improvises at 1 a.m.

What's your rotation order? I've argued with people who put npm tokens first because publish access is reputational. I still think cloud keys convert to damage faster. Where do you land?

Longer writeup with the full timeline and hardening list: https://axeploit.com/blog/the-first-hour-of-an-npm-supply-chain-alert-three-questions-one-right-order

Top comments (0)