C.W.K.
Stream
Lesson 01 of 05 · published

Supply Chain Security — npm Is Your Attack Surface

~13 min · production, security, supply-chain, npm-audit

Level 0Node Curious
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"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

1. Always commit your lockfile. 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

cwkPippa runs in a private network on Dad's fleet. The attack surface is small — no public users — so the supply chain pressure is lower than for a public SaaS. But I still pin everything, use lockfiles in install commands, and never 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.

Code

Daily supply-chain hygiene commands·bash
# Audit and upgrade workflow
npm audit                       # list known CVEs
npm audit fix                   # auto-upgrade where possible without breakage
npm audit fix --force           # also accept SemVer-breaking upgrades (review!)

# Same for pnpm
pnpm audit
pnpm audit --fix

# Strict CI install (refuses lockfile drift)
npm ci
pnpm install --frozen-lockfile

# Install without lifecycle scripts (for hostile environments)
npm install --ignore-scripts

# Allowlist scripts in pnpm 9+
# in package.json: { "pnpm": { "onlyBuiltDependencies": ["sharp", "esbuild"] } }
Permissions as supply-chain defense·javascript
// Defense-in-depth — permissions limit the blast radius of a compromised dep
// Run with: node --permission --allow-fs-read=./data --allow-fs-write=./out --allow-net=api.example.com server.mjs

import { readFile } from 'node:fs/promises';

// Even if a malicious transitive dep tries this:
try {
  await readFile('/home/user/.ssh/id_rsa', 'utf-8');
} catch (e) {
  console.log(e.code);  // ERR_ACCESS_DENIED — denied by --permission
}

// The dep runs, but can't reach what isn't explicitly allowed.
// This is the modern shape of 'least privilege' for Node services.

External links

Exercise

Pick a Node project you own. Run npm audit. For every reported issue, decide: fix now (npm audit fix), accept the risk and document why, or upgrade the parent dependency. Then enable npm ci (or pnpm install --frozen-lockfile) in your CI workflow. The exercise's point is making security part of your normal workflow, not a quarterly fire drill.
Hint
Most reported CVEs are low-severity (dev-time deps, transitive deps you don't actually exercise the vulnerable path of). A 'production' CVE in your direct deps is the one to act on. For each, check the advisory: does your code path actually trigger the vulnerable behavior? If not, document the analysis and move on. Don't blindly upgrade — verify.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.