"In a typical Node app, 99% of the code that runs in production was written by someone you've never met. Every npm install is a trust decision."
The Attack Surface
Your code is maybe 5,000 lines. Your node_modules after a typical npm install is 200MB and includes 800 packages from 300 different authors. Every one of those packages can:
- Run arbitrary code at install time (via npm lifecycle scripts).
- Run arbitrary code at import time (via top-level side effects).
- Make network requests, read files, spawn processes — anything Node can do.
Most do none of this. But a few have, historically. The npm registry has seen typosquatting, account takeovers, malicious updates to popular packages. The supply chain is real.
The Five Habits That Actually Matter
package-lock.json / pnpm-lock.yaml pins every package to a specific version + checksum. Without it, you get a different dependency tree each install — and a different attack surface.2. Use
npm ci (or pnpm install --frozen-lockfile) in CI. Refuses to install if package.json and lockfile disagree. Stops silent drift.3. Run
npm audit regularly. Lists known CVEs in your dependencies. Not perfect — false positives for non-exploitable issues, false negatives for unreported bugs — but it's the floor.4. Disable lifecycle scripts you don't need.
npm install --ignore-scripts or pnpm's onlyBuiltDependencies allowlist (pnpm 9+) prevents arbitrary postinstall code from running. Audit which deps need their scripts; allowlist only those.5. Pin your tooling, not just your runtime deps. A compromised devDependency can write malicious code into your build output before it ships. Pin TypeScript, ESLint, Vite, the works — they're as security-critical as your runtime deps.
The Permissions Model as Defense in Depth
Track 6's permissions model is your last line: even if a malicious dep gets executed in your process, node --permission --allow-fs-read=./data ... limits what it can read or write or call out to. A compromised dep that tries to read ~/.ssh/ hits ERR_ACCESS_DENIED and fails loudly. Worth using even when you trust your deps.
The Lockfile Is Not Magic
Pinning to v1.2.3 means "version 1.2.3 of this exact tarball." If 1.2.3 was the malicious release, the lockfile pins you to the malicious code. Lockfiles protect against silent updates, not against compromised releases. The defense for that is to install old, slowly: don't upgrade deps the day they release, wait a week or two, let the community vet them. npm-check-updates with manual review beats npm update --latest.
What npm audit Misses
npm audit checks the GitHub Advisory Database. It misses:
- Unreported vulnerabilities (most of them).
- Vulnerabilities in packages without active advisories.
- Logic-level issues (deps doing legitimate-but-unwanted things).
- Malicious code in fresh releases not yet flagged.
Treat npm audit as a floor. For higher confidence, tools like socket.dev or snyk analyze packages more broadly (network behavior, lifecycle scripts, dependency drift). Worth the cost for production services handling sensitive data.
Pippa's Confession
npm install --latest without reading changelogs. Dad's framing: "Even with low blast radius, sloppy supply chain habits propagate. Build the habits in private repos so they're automatic when you're in a public one." The cost is small; the upside compounds.