The Attack Surface Is Your Dependency Tree
The code I wrote last week is maybe two thousand lines. The code that actually ships is two thousand lines plus about four hundred packages I have never opened, written by people I will never meet, each of which pulls in more of the same.
That is not a complaint about open source. It is the only reason any of us ship anything. But it does mean the question “is my application secure” has very little to do with the code I wrote.
How the attack actually works
A dependency attack is boring and effective. Somebody takes over a maintainer account — reused password, phished token, or a package handed over by a burned-out author who is happy to see it go. Then they publish a new patch version with a few extra lines.
Those lines run on your build machine the moment you install, or inside your app at runtime. Nobody reviews them, because the changelog says “fix typo in README”.
The pattern repeats every few months with a different package name. The details change; the shape does not.
Why “npm audit” is not the answer
npm audit compares your tree against a database of known vulnerabilities. It is useful, and you should run it. But a malicious package is not a known vulnerability on day zero — it is a perfectly valid package that does something nasty.
So audit catches the boring half of the problem (a library with a published CVE) and misses the half that actually hurts (a fresh release with a payload). Treat it as a smoke detector, not a security strategy.
What actually reduces the risk
These are the things I apply on my own projects, in rough order of payoff per hour spent.
1. Commit the lockfile and install from it. npm ci instead of npm install in CI, pip install --require-hashes where you can. The lockfile is the only record of what you actually ran.
2. Turn off install scripts unless you need them. Most packages do not need to run code at install time. --ignore-scripts breaks a few native builds and blocks a whole category of attack. Worth the occasional exception.
3. Read the diff when a dependency updates. Dependabot and friends are great at opening the pull request and terrible at reading it. A patch bump to a package that has not changed in four years deserves a look.
4. Prefer fewer, older dependencies. A package with three million weekly downloads and ten years of history is a bigger target but a much better-maintained one than a clever utility with forty stars. Download counts are a weak signal; abandonment is a strong one.
5. Check who can publish. Two-factor on your own registry account, and a private proxy or internal registry for anything critical, so a compromised public release cannot reach your build until someone approves it.
6. Pin the things that can hurt you. A payments library, an auth library, anything that parses untrusted input. Vendoring or a strict version pin is not paranoia when the blast radius is your users’ data.
7. Know what you ship. Generate an SBOM. Not because a customer asked, but because when the next incident lands you want to answer “are we affected” in ten minutes with a query instead of a week with grep.
The uncomfortable part
None of this makes you safe. A determined attacker with a maintainer account and a plausible commit message gets through most of it.
What it does is raise the cost and shorten the window. Most real incidents are caught by somebody noticing a weird network call or an unexpected postinstall script, not by a scanner. If your build is reproducible and your dependencies are pinned, that noticing is possible. If your build pulls whatever is latest at 2am, you are debugging a moving target.
What I do now, concretely
- Lockfile committed,
npm cieverywhere, no exceptions. - Install scripts off by default; I opt in per package with a comment explaining why.
- A weekly dependency pull request that I actually review, instead of a daily pile I ignore.
- Any package under a month old gets a second look before it enters a project I care about.
- 2FA on every registry account I own, including the ones I forgot I had.
The dependency tree is the part of the system nobody on the team owns, and it is the part with the most strangers in it. Treating it like production code — reviewed, pinned, and reproducible — is the cheapest security work available.
See you in the next one.