Dependencies and the supply chain

Know what you are shipping, and shrink the amount of other people’s code that can hurt you.

11of 16
≈ 12Min read
5Parts

Most of your application is not yours

A typical project has a few thousand lines of its own code and tens of thousands of packages' worth of everyone else's, pulled in transitively. Every one of them runs with the same privileges as your code — during the build, on the server, and in the browser.

This is not an argument for writing everything yourself. It is an argument for knowing what is in there, keeping it current, and reducing it where the cost is low.

The lockfile is the inventory

Without a lockfile, two installs a week apart give two different applications, and a compromised patch release arrives silently. With one, the set of code is pinned, reviewable in a diff, and reproducible on every machine.

npm ci fails if the lockfile and package.json disagree — which is exactly the behaviour you want in a pipeline.

and it belongs in the repository
package-lock.json / pnpm-lock.yaml / yarn.lock

npm ci          # install exactly the lockfile — the only thing CI should run
npm install     # may update the lockfile, which is not what a deploy wants

Known vulnerabilities

Most advisories will not apply to you — a denial of service in a parser you never expose, a prototype pollution in a dev-only tool. Read the advisory, decide, and record the decision. A team that treats every audit as an emergency stops reading them within a month, which is worse than not running it.

the routine, not the panic
npm audit                  # what is known, and what fixes it
npm audit fix              # the safe upgrades
npm outdated               # how far behind you are

# and in CI, weekly, with a bot that opens the pull request
Upgrade little and often

The cost of an upgrade grows faster than linearly with how far behind you are. A weekly bot with tests you trust turns a yearly migration project into a Tuesday.

The attacks that are not "a known CVE"

npm ci --ignore-scripts removes the whole install-script class, at the cost of packages that genuinely need to compile something. Scope your internal packages (@doors/config) and configure the registry, so a public package of that name cannot be substituted.

AttackWhat happens
Typosquattinglodahs, react-dom-router, crossenv — a name one character from a popular one
Dependency confusionyour internal package name exists on the public registry too, and the public one wins
A compromised maintainera real package, a real account, a malicious patch release at 2 a.m.
Install scriptspostinstall runs arbitrary code the moment you install — before any of your code exists
A protestware updatethe maintainer decides to make a point, and your build does something unexpected

Reducing the surface

  • Count before adding. A package that pulls sixty transitive dependencies to left-pad a string is a bad trade; one that pulls none and saves a week is a good one.
  • Prefer boring and maintained over clever and abandoned. Look at the last release, the open issues, and how many people can publish.
  • Pin build-time tools as tightly as runtime ones — they run on your machine with your credentials.
  • Separate dev and production dependencies, and install only production ones on the server.
  • Generate an SBOM if anyone asks you what you ship. npm sbom, or CycloneDX.

Practice