The Software Supply Chain

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.

The big question

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

Tests — Run & Watch Them Pass

Run to execute the tests…

Everyday analogy

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

Tests — Run & Watch Them Pass

Run to execute the tests…

From the archives

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

Tests — Run & Watch Them Pass

Run to execute the tests…

Engineer's notebook

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

  1. Reviewing only direct dependencies. They are about 1% of the tree; 1,171 of 1,183 packages arrived without a decision.
  2. 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.
  3. Ranking findings by CVSS instead of reachability. Half the criticals may be unexploitable in your code while a medium is live.
  4. Letting scanner noise train the team to ignore alerts. A 75% false-positive rate is itself the vulnerability.
  5. Dismissing unreachable findings permanently. Reachability changes when your code changes, and nobody re-runs the analysis.
  6. Adding a twelve-line dependency. The trust relationship is permanent; the saving is not.
  7. Leaving install scripts enabled. Arbitrary code runs with developer or CI privileges before any review or test.
  8. Choosing packages by download count. event-stream had millions of weekly downloads when it was backdoored.
  9. Upgrading rarely. Deferred upgrades accumulate into one unreviewable diff, usually done under CVE pressure.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Warm-up
What is a supply-chain attack, and why are dependencies a target?
Model answer — It is an attack that reaches you through something you trust rather than through your own code — a library, a build tool, a base image, a CI action. Dependencies are attractive because of leverage: compromising one popular package puts code inside thousands of organisations at once, whereas attacking those organisations individually takes thousands of separate efforts. And the trust is largely unexamined — a typical project has maybe a dozen direct dependencies but over a thousand packages in the installed tree, so about ninety-nine percent of what runs arrived without anyone deciding to trust it. Each of those packages has maintainers who can publish a new version, which means a very large number of accounts can cause code to execute in your build. The attacks that have actually worked exploit that: publishing a typosquatted name that differs from a real package by two transposed letters, or taking over an abandoned package when a tired maintainer hands it to a helpful-seeming volunteer, which is what happened with event-stream. Neither of those requires breaking anything technical.
Intermediate
Your scanner reports 40 vulnerabilities. How do you prioritise?
Model answer — Not by CVSS score, because that ranks the vulnerability in the abstract rather than in my system. The first question is reachability: does our code actually call the vulnerable path? In practice most reported issues are in code paths the application never executes — a CLI entry point we do not ship, a parser for a format we never handle, a dev dependency that is not in the production process at all. Something like three quarters of a typical report falls away on that test, and the remaining handful is a queue a team will genuinely work through, which matters because a report that is mostly noise trains everyone to stop reading it — at that point the process failure is worse than any individual unpatched library. So: split production from development scope, run reachability analysis if the tooling supports it and do it by hand for the top findings if not, and rank by whether an attacker can actually get there and what it gives them. Two caveats I would state. Development dependencies are not harmless — they run in CI with CI's credentials, and CI can usually deploy to production, so I would treat build-time reachability as a separate axis rather than assuming dev means safe. And unreachable findings get deprioritised, not dismissed, because reachability changes the moment someone refactors and nobody re-runs the analysis. If the upgrade is cheap I take it regardless, since the cost of staying current is much lower than the cost of a forced emergency upgrade later.
Advanced
How would you reduce supply-chain risk across an organisation with hundreds of services?
Model answer — I would start by rejecting the framing that this is solved by auditing dependencies, because at that scale it is arithmetically impossible and any plan built on it fails quietly. The realistic goals are reducing the surface, removing the execution paths attacks rely on, and being able to answer questions fast when something happens. Concretely, in rough priority order. First, disable install scripts by default with a small reviewed allowlist — that removes the mechanism most published payloads use, and it is one platform change rather than hundreds of team decisions. Second, run an internal registry that proxies the public ones, so you get a cache that survives a package being yanked, a chokepoint where policy applies, and the ability to quarantine a version organisation-wide in one action instead of filing hundreds of tickets. Add a cooling-off rule there — nothing published in the last few days is installable — which catches a meaningful fraction of malicious releases, since most get detected and pulled within hours. Third, generate an SBOM per build and store it centrally, because the question that actually gets asked during an incident is "which of our services contain this package", and without an inventory that takes days of manual work instead of minutes. Fourth, reachability-based triage rather than raw CVE counts, so the queue stays credible. Fifth — and this is the one that gets skipped — treat CI as production: it holds deployment credentials, it runs dependency code, and it is usually reviewed far less carefully than the systems it deploys to. On the organisational side, the thing that determines whether any of this works is whether it is centralised. Guidance that asks every team to do the right thing produces compliance proportional to how busy each team is, which is not a security posture. The controls have to live in the platform — the registry, the base images, the CI templates — so that the secure path is the default path and teams opt out visibly rather than opting in silently. I would also be explicit with leadership about what this does not cover: none of it detects a well-targeted backdoor in a package we already trust, which is the event-stream case, and the honest answer there is detection and response rather than prevention.
Company lens — how the same topic gets asked
micro1Concept: what a supply-chain attack is, why lockfiles matter and what they miss.
GoogleDepth: reachability analysis, install-script execution, registry-level controls.
MetaDesign: org-wide policy via internal registry, SBOM inventory, CI as production.
AmazonReality: an emergency upgrade under CVE pressure, and what made it painful.
Hands-on

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.

Tests — Run & Watch Them Pass

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-stream had 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.

Editorial reflection

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.