Lesson 1 of 5
Dependency Risk
"Attackers don't break in through the front door anymore. They come in through the seventeen doors you didn't know you had." — paraphrasing a common refrain in supply-chain security research
Every chapter so far has been about code you wrote. This Part is about the code you did not, which in a modern application is nearly all of it.
Open a typical Node service and count. Twelve direct dependencies — packages a human on your team chose, evaluated, and typed into a file. Install them and the tree resolves to 1,183 packages. That is a ratio of 98.6 to 1: for every package someone decided to trust, roughly ninety-eight arrived as a consequence. You chose 1% of what runs in your process. The other 1,171 came along.
The security consequence is not that those packages are malicious — the overwhelming majority are ordinary, useful and fine. It is that each one is a person with commit access to your production runtime. At a conservative two maintainers per package, that is 2,366 people who can, by publishing a version, cause code to execute inside your build and your servers. None of them signed a contract with you. Most have never heard of you. Any one of them can have their account compromised, get bored and hand the project to a stranger, or turn out to have been the stranger all along.
This chapter is about reasoning proportionately about that. Not "audit every dependency," which is impossible, and not "dependencies are fine," which is how the last decade's most effective attacks worked. Something in between, based on where the leverage actually is.
Your lockfile pins every package to an exact version with an integrity hash, so a malicious maintainer publishing a new release cannot affect you until someone runs an upgrade. Does that make you safe? Name the attack the lockfile stops and the attack it does not.
The shape of the tree
Run to execute the tests…
Hiring a contractor to renovate your kitchen is a decision you make carefully — references, a quote, a conversation. The contractor then subcontracts the plumbing, the plumber brings an apprentice, the apprentice's tool hire company sends a driver, and by Tuesday there are thirty people who have had keys to your house and you interviewed one of them. Nothing about that is unusual or sinister; it is how construction works, and the alternative — personally vetting the driver — is not available to you. What you can reasonably do is limit how many contractors you hire, ask the one you did hire who else will be coming, and notice if the crew changes. That is the honest scope of dependency security: you are managing a delegation chain, not verifying it.
Vulnerability alerts are mostly noise, and that is the problem
Run to execute the tests…
In 2018 the maintainer of event-stream, a JavaScript package with millions of weekly downloads, grew tired of maintaining it and handed publishing rights to a volunteer who had been submitting helpful patches. The volunteer added a dependency, then published a version of that dependency containing obfuscated code targeting a specific Bitcoin wallet application — it decrypted its payload using a key derived from the victim application's own package description, so it did nothing at all on any other machine and was therefore invisible in casual inspection. It ran for roughly two months. What makes this the defining supply-chain case study is that no system was breached: the transfer of maintainership was voluntary, public and entirely normal open-source practice. The attack exploited the social layer, and the defence — noticing that a package changed hands — is not something any lockfile, scanner or integrity hash observes.
Choosing what to depend on
Run to execute the tests…
The single change I have seen produce the most security value per hour of effort is disabling install scripts by default. It takes an afternoon: set ignore-scripts, run a build, find the three or four packages that genuinely need native compilation, allowlist those explicitly, and document why each one is on the list. What it buys is the removal of the execution path that most published supply-chain payloads use — code running with a developer's full local privileges, or CI's credentials, before a single test has run and before anyone has looked at a diff. The reason it is not the default everywhere is that it breaks a handful of popular packages loudly, which makes it feel risky, whereas the status quo fails silently and therefore feels safe. That inversion — the safer option looking scarier because its failures are visible — recurs throughout this Part.
Common Mistakes
- Reviewing only direct dependencies. They are about 1% of the tree; 1,171 of 1,183 packages arrived without a decision.
- Treating a lockfile as protection. It stops a malicious new release and does nothing about a version that was already compromised, or about install scripts.
- Ranking findings by CVSS instead of reachability. Half the criticals may be unexploitable in your code while a medium is live.
- Letting scanner noise train the team to ignore alerts. A 75% false-positive rate is itself the vulnerability.
- Dismissing unreachable findings permanently. Reachability changes when your code changes, and nobody re-runs the analysis.
- Adding a twelve-line dependency. The trust relationship is permanent; the saving is not.
- Leaving install scripts enabled. Arbitrary code runs with developer or CI privileges before any review or test.
- Choosing packages by download count.
event-streamhad millions of weekly downloads when it was backdoored. - Upgrading rarely. Deferred upgrades accumulate into one unreviewable diff, usually done under CVE pressure.
Interview Corner
Count your tree: compare the number of direct dependencies in your manifest with the total installed (npm ls --all | wc -l, pip freeze | wc -l, or your ecosystem's equivalent). Write down the ratio. Then list which of your packages run install scripts, and try a build with scripts disabled — note which ones break and whether each genuinely needs native compilation. Take your latest scanner report and classify the top ten findings by whether your code actually reaches the vulnerable function; count how many survive. Finally, pick your three most critical direct dependencies and check, for each, how many maintainers it has and when it last changed hands.
Run to execute the tests…
Summary
- You choose about 1% of what runs. Twelve direct dependencies resolve to 1,183 packages — a 98.6× ratio, with 1,171 arriving without a decision.
- The real unit is people, not packages. At two maintainers each, that tree is 2,366 accounts whose compromise executes code in your build.
- Review reaches one of seven depth levels — 85.7% of the tree is trusted transitively on an assumption nobody upstream actually honoured.
- A lockfile stops a malicious new release and nothing else. It does not help if the pinned version was already backdoored, and install scripts still run.
- Rank by reachability, not CVSS. Of eight findings only two were reachable — 75% noise — and half the criticals were unexploitable while a medium was live.
- Scanner noise is itself a vulnerability, because it trains a team to stop reading the report.
- Disabling install scripts is the highest-leverage single change: it removes the execution path most published payloads rely on, and costs an afternoon plus a short allowlist.
- Popularity predicts nothing.
event-streamhad millions of weekly downloads when a tired maintainer handed it to a stranger — an attack on the social layer that no technical control observes. - Upgrade frequently in small diffs, and apply a cooling-off period, because most malicious releases are detected and pulled within days.
The next chapter narrows to the single most valuable thing an attacker can extract from your build: the credentials it holds. CI has production access, runs untrusted code, and is reviewed less carefully than anything it deploys to.
What makes supply-chain security genuinely different from the rest of this volume is that the honest answer includes an admission of limits. Everywhere else in security engineering there is a defensible position — validate the input, check the authorisation, verify the signature — and a team that does the work can say the class of bug is handled. Here there is no such position. You cannot audit 1,183 packages, you cannot verify 2,366 maintainers, and any plan whose success depends on doing so is a plan that will be quietly abandoned in month two. What you can do is reduce the number of things you depend on, remove the execution paths attacks use, make change visible, and be able to answer "where is this package" in minutes rather than days. That is a materially weaker guarantee than "we are not vulnerable," and I think the discipline is in saying so out loud rather than dressing up a scanner report as assurance. The teams I have seen handle this well are the ones that stopped trying to prove their dependencies are safe and started building the capacity to respond quickly when one turns out not to be — which is, not coincidentally, the shift that Part VII is about.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.