DEV Community

Simon Kobler
Simon Kobler

Posted on

Stop letting npm install run untrusted code on your machine — meet np-audit


npm install
Enter fullscreen mode Exit fullscreen mode

You type it dozens of times a day. You probably typed it this morning. And every time you did, you handed arbitrary code execution to every maintainer in your dependency tree — and every attacker who has phished one of them.

Over the last eight months, attackers have noticed. The Shai-Hulud family of worms has compromised hundreds of npm packages, created tens of thousands of malicious GitHub repos, and harvested thousands of developer secrets. The November 2025 wave alone hit 700+ packages, 27,000 malicious repos, and ~14,000 exposed secrets across 487 organizations — in under 48 hours.

I got tired of waiting for npm to fix this, so I built np-audit — a zero-dependency CLI that statically analyzes install scripts before npm executes them. This post is partly about why you need it, and mostly about how to use it.


The attack that keeps working

Every Shai-Hulud variant relies on the same three lines:

{
  "scripts": {
    "preinstall": "node setup.mjs"
  }
}
Enter fullscreen mode Exit fullscreen mode

That's it. The second npm install resolves a compromised version, setup.mjs runs before your code, before your tests, before any human looks at anything. Same user, same env vars, same network access as you.

The payload is always some flavor of the same recipe:

  1. Download a second runtime (Bun is the current favorite — bypasses Node-pattern detection).
  2. Run an obfuscated harvester that scans for GitHub tokens, npm credentials, AWS/Azure/GCP keys, Kubernetes service account tokens, Vault creds, browser-saved passwords, and CI runner secrets.
  3. Encrypt and exfiltrate by pushing to public GitHub repos via the GraphQL API — looks like normal git activity from the outside.
  4. Use any stolen GitHub PAT to inject malicious workflows into other repos the victim can write to. One dev becomes patient zero for their entire org.

A nice touch from the Mini Shai-Hulud variant:

if (locale.startsWith('ru') || lang.startsWith('ru')) {
  process.exit(0); // do nothing
}
Enter fullscreen mode Exit fullscreen mode

The same Russian-locale guardrail appears in three separate campaigns now attributed to TeamPCP.


This is not a vulnerability. This is the feature.

There is no CVE to patch here. npm preinstall, install, and postinstall scripts run automatically by design — that's how packages compile native addons, fetch platform binaries, set up build tooling. It's documented, intentional, and useful.

It's also a loaded gun in every Node project on Earth, and it has been getting fired regularly since 2018: event-stream, ua-parser-js, node-ipc, colors, faker, the Bitwarden CLI in April. The attackers aren't getting more sophisticated. They don't need to.

--ignore-scripts is the official advice. It also breaks bcrypt, node-sass, puppeteer, sharp, and half your toolchain. Nobody actually runs it in CI.

So we live with the gun pointed at us. Or we look at what the scripts actually do before we let them run.


Meet np-audit (npa)

np-audit is a static analyzer for npm lifecycle scripts. It downloads the tarballs npm is about to install, reads every preinstall / install / postinstall script, and flags the patterns that every documented supply chain attack has used:

  • eval() and new Function() calls
  • Obfuscator.io-style mangling (var _0x3f2a = [...])
  • High-entropy strings (encrypted/compressed payloads)
  • Hex escape density and String.fromCharCode() chains
  • Buffer.from(x, 'base64') followed by eval
  • Shell spawning via child_process
  • process.env access combined with outbound network calls

Each signal contributes to a score. Anything over a configurable threshold blocks the install. Zero runtime dependencies, pure Node built-ins, and — obviously — no install scripts of its own. The whole point of a supply chain auditor is that you can audit it in an afternoon.

Install

npm install -g np-audit
Enter fullscreen mode Exit fullscreen mode

Daily use

Just swap the verb:

npa install   # audit, then npm install
npa ci        # audit, then npm ci
npa scan      # audit only, no install
Enter fullscreen mode Exit fullscreen mode

If a package is suspicious, you get a clean report and a non-zero exit code — drop-in safe for CI:

✗ evil-pkg@1.0.0  postinstall: install.js  DANGER (score: 9)
Enter fullscreen mode Exit fullscreen mode

Interactive review for the paranoid

npa i --review
Enter fullscreen mode Exit fullscreen mode

Drops you into a TUI listing every install script in your tree. You decide one by one which ones get to run. Under the hood it's npm install --ignore-scripts followed by manual execution of only the scripts you approved — basically informed consent for lifecycle scripts.

Set and forget

npa alias --install
Enter fullscreen mode Exit fullscreen mode

Installs a shell hook so every npm install and npm ci you type is scanned first. Clean tree, npm proceeds. Suspicious tree, npm never runs.

$ npm install lodash
[npa] Scanning dependencies before npm install...
✔ No packages with install scripts found.
[npa] Scan passed. Running npm install...
Enter fullscreen mode Exit fullscreen mode

CI example

GitHub Actions:

- name: Install dependencies (audited)
  run: |
    npm install -g np-audit
    npa ci
Enter fullscreen mode Exit fullscreen mode

If a transitive dep gets compromised between your last green build and this one, the job fails before the malicious script touches your runner's env vars.


What it doesn't do

np-audit is not magic. A determined attacker writing clean, readable, plain-JavaScript malware can slip past a static heuristic check — that's a fundamental limit of static analysis.

The point isn't to be perfect. The point is to raise the cost from "drop in a preinstall and harvest 14,000 secrets in a weekend" to something that requires real effort. Every Shai-Hulud variant so far has leaned on heavy obfuscation precisely because the maintainers were trying to slip past human review. np-audit is human review at machine speed.


TL;DR

  • npm install runs untrusted code on your machine. This is by design.
  • The Shai-Hulud worms are exploiting that design at industrial scale and getting away with it.
  • --ignore-scripts breaks too much to be realistic.
  • np-audit looks at install scripts before they run, scores them, and blocks the obviously malicious ones.
npm install -g np-audit
npa ci
Enter fullscreen mode Exit fullscreen mode

Issues, PRs, and stars welcome: github.com/KoblerS/np-audit

Stay safe out there. And maybe read the next preinstall script before you let it read your ~/.aws/credentials.

Top comments (0)