Ken Thompson’s 1984 Turing Award lecture, Reflections on Trusting Trust, described a C compiler modified to insert a backdoor into the login program, then modified again so the compiler would replicate the backdoor in future versions of itself without any trace in the source. The source was clean, the binary was compromised, and the only way to discover the backdoor was to rebuild the entire compiler toolchain from scratch and compare the output, which nobody was going to do.
The explosion of open source was built on this kind of transitive trust between maintainers. A package with 800 transitive dependencies works because each maintainer along the way did a reasonable job of choosing and maintaining their own dependencies, and the maintainers they depended on did the same. Nobody designed this trust network or audited it as a whole. It just grew as people built on each other’s work, and it has held up well enough that we’ve come to take it for granted, even as bad actors have started to map its weak points.
We have decent tools now for scanning our own dependency trees. You can run npm audit or Dependabot or Snyk against your lockfile and get a report on known vulnerabilities. But when you do that, you’re trusting that the maintainer of each package in your tree is doing the same: running audits, reviewing what they pull in, dropping dependencies whose maintainers have gone quiet, keeping their build tooling current. And you’re trusting that those maintainers are trusting their own dependencies’ maintainers to do the same, all the way down through a chain of people who mostly don’t know each other and have no visibility into each other’s practices.
Every package you install was also built, tested, and published using dependencies you never see: a JavaScript library’s devDependencies, the build tools that compiled a Rust crate before it was uploaded, the pytest plugins that ran during CI, the GitHub Action that handled publishing. You’re trusting that the maintainer chose those carefully, keeps them updated, and drops them when they go stale, and that the maintainers of those tools are doing the same. A maintainer who never runs npm audit, who has a three-year-old GitHub Action in their publish workflow, who accepted a PR from a stranger adding a new build dependency without much scrutiny, produces an artifact on the registry that looks identical to one from a maintainer who checks everything meticulously.
The event-stream incident is the classic example: the original maintainer handed the project to someone new, that person added a malicious dependency, and nobody upstream noticed. The xz backdoor was more patient and more frightening. A co-maintainer spent two years making legitimate contributions before planting obfuscated code in the build system and test fixtures, targeting a part of the toolchain that almost nobody reads. And then there’s the codecov bash uploader compromise, which didn’t target a library at all but a CI tool that thousands of projects were curling into their build pipelines. I suspect most maintainers who used it never read the script once.
Trusted publishing is an effort to close part of this gap. PyPI, npm, and RubyGems now support publishing flows where packages are built and uploaded directly from CI using short-lived credentials tied to a specific repository and workflow, which creates a verifiable link between the source and the published artifact. But it also means we’re now trusting that each maintainer’s CI configuration is sound, that the GitHub Actions in their workflow are maintained by people who are themselves doing due diligence, that the dev dependencies installed during the build are ones they’ve reviewed. GitHub Actions in particular has almost none of the supply chain protections that language package managers have spent years building, so in practice we’ve traded one unverifiable assumption for a different one.
Semver ranges compound this because npm update or bundle update or cargo update will pull in new versions across your entire tree in seconds, and you’re trusting that every maintainer in the chain shipped something good since the last time your lockfile was generated, including versions built against whatever state their toolchains were in at the time.
Large companies deal with this by vendoring and rebuilding everything from source in controlled environments, effectively verifying each level of the trust chain themselves instead of relying on each maintainer to have done it. But even vendoring just moves the boundary. Those controlled builds still run on compilers and operating systems and hardware that somebody else produced, and at some point you stop verifying and start trusting. The honest version of “we’ve audited our supply chain” is “we’ve audited our supply chain down to a depth we felt comfortable with and then stopped”.