The dependency I didn't choose
On 14 decisions producing 1,100 dependencies, why the risk isn't code quality, and the distinction between build-time and run-time that nobody bothers to make.
The build broke. Nothing had changed on our side, which is always the sentence that precedes an afternoon.
The trail went: our project depends on a build tool, which depends on a plugin, which depends on a utility library, which depends on a small package that does one thing with file paths, and that small package had shipped what its author reasonably considered a patch and which changed a return value in one edge case.
I’d never heard of it. Nobody on the project had. It has one maintainer, who did nothing wrong, and who owes us precisely nothing.
14 decisions, 1,100 dependencies
I went and counted afterward, on a fairly ordinary project.
14 direct dependencies. Things I chose, evaluated, read about, made a decision on: the framework, the build tool, an image plugin, a date library, a linter.
Somewhere north of 1,100 packages in the tree in total.
So of the decisions represented in that folder, I made about 1% of them. I inherited the rest, transitively, from people who were making perfectly reasonable choices for their own projects. Each of those people did the same thing I did: evaluated four or five things carefully and accepted a few hundred implicitly.
I made fourteen decisions and got eleven hundred dependencies. Every one of the other 1,086 was somebody else's reasonable choice, made for their project rather than mine.
I don’t think this is straightforwardly bad, and I want to say that clearly before I get gloomy, because “just write it yourself” is how you get a worse date library with more bugs in it and no test suite. Reuse is why a small agency can build things that would have taken a team five years ago. That’s real and I’m not giving it up.
But it’s worth being accurate about what we’ve actually agreed to.
The risk isn’t code quality
The instinctive worry is that some of those thousand packages are badly written, and honestly that’s the least of it. Most are fine, and a bug in one produces a broken build: annoying and visible and fixable in an afternoon, like mine.
The real exposure is that each package is an account. A username and a password belonging to a person, at a registry, that can publish new code which lands on thousands of build machines automatically. The security model of the whole system is “these individuals have not had their credentials stolen and are not having a bad year.”
There have been a couple of demonstrations of exactly that going wrong over the years, and the one that’s been on my mind since February is the dependency confusion work, where a researcher published packages to the public registry using the names of companies’ internal packages, and a number of large organizations’ build systems preferred the public one and ran his code. Nobody was breached in the usual sense. The tooling just resolved a name the way it was designed to.
That’s not a bug in anybody’s code. It’s the shape of the system.
Build-time and run-time are different risks
Here’s the distinction I think is under-made, and it changed how much I worry.
On a static site, almost none of those 1,100 packages ever reach a visitor. They run on my machine and on the build server, produce some HTML and CSS, and then they’re irrelevant. The output ships; the tooling doesn’t.
Which means the attack surface for a compromised build dependency is my laptop, the build server, and whatever credentials are in its environment. That’s serious, and it’s a completely different threat from something that runs in a customer’s browser while they type their card number.
Meanwhile a single tag added through the client’s tag manager runs on every visitor, on every page, with full access to the DOM, from a domain I don’t control, and nobody audited it at all.
So the proportion of anxiety in this industry is a bit odd. We have audit commands and automated update pull requests for the tooling, the lower-exposure half, and essentially no process whatsoever for the third-party JavaScript we deliberately put on the page, the higher one. I wrote a whole post about measuring those and it was framed as a performance problem, and about half of it should probably have been a security post.
What actually helps
Not much is dramatic. In rough order of value.
Fewer direct dependencies. This is by far the biggest lever, because each one you decline closes an entire subtree. Before adding something, look at what it drags in. A package with two dependencies and a package with 90 are not comparable purchases even if they do the same job.
Commit the lockfile and install from it. npm ci rather than npm install in CI, so the build gets exactly what’s recorded and fails loudly if the two files disagree. This was the entire argument for lockfiles back when they arrived, and it also means my Tuesday afternoon could not have happened.
Update on a schedule, deliberately. Both extremes are bad. Automatic merging of update pull requests means you’re auto-applying code from a thousand strangers, which is the thing you were worried about. Never updating means you’re on five-year-old code with known holes. What’s worked for me is batching them monthly, reading the changelogs of the direct ones, and letting the transitive ones ride along.
Vendor the tiny ones. If a dependency is 40 lines and does something obvious, copy the 40 lines into your project with a comment saying where they came from and under what license. You’ve traded a supply chain for 40 lines you now own, and on a small project that’s a good trade more often than the culture admits.
The uncomfortable version
The bit I keep coming back to.
The package that broke my build is maintained by one person, in their own time, for free, and it is depended on by an enormous number of things. If they get bored, or ill, or simply stop, a large amount of infrastructure has a problem. If somebody guesses their password, a lot of build machines run whatever gets published.
We’ve collectively decided that’s acceptable, and mostly it is, because these people are conscientious to a degree that has no business being reliable. But the exposure isn’t really technical. It’s that the people holding a lot of this up aren’t being paid, and a tired unpaid person is a worse security control than any tool you could buy.
I don’t have a fix. I’ve started sending a bit of money to two of them. Gestural, and I’d rather gesture than not.
Forty minutes, and then an evening
Pinned the version, fixed the build, moved on.
And then spent the evening looking at the tree, which I’d recommend doing once, soberly, on your main project, in the same spirit as reading your own bank statement. It won’t change what you do very much. It’s just worth knowing what you’ve signed.