Skip to content
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 Permission Model Is a Seat Belt, Not a Sandbox

Track 6's permission model can stop unintended access through covered APIs. For example, trusted application or dependency code that accidentally reads outside --allow-fs-read=./data receives ERR_ACCESS_DENIED. That is useful defense in depth, but Node explicitly does not treat it as protection from malicious code: hostile code can bypass the model, and some APIs sit outside particular checks. --allow-net also opens network access generally rather than limiting it to named hosts. Use operating-system isolation when untrusted code is in scope.

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"] } }
Permission-model boundary for trusted code·javascript
// Defense in depth for unintended access through covered APIs
// Run with: node --permission --allow-fs-read=./data --allow-fs-write=./out --allow-net server.mjs

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

// Trusted dependency code that accidentally reaches outside policy:
try {
  await readFile('/home/user/.ssh/id_rsa', 'utf-8');
} catch (e) {
  console.log(e.code);  // ERR_ACCESS_DENIED — denied by --permission
}

// This is a seat belt for trusted code, not a sandbox for malicious code.
// Use OS-level isolation when the code itself is untrusted.

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.