The build makes a trust decision on every install

Every time your build runs, it fetches code — dozens or hundreds of third-party packages, each pulling more of its own. That is normal and unavoidable; nobody writes everything from scratch. But buried in every install is a decision the build makes automatically: which package, from which source, at which version. Most of the time that decision is invisible and correct. Dependency confusion is what happens when an attacker learns to make it wrong.

The trick is unnervingly simple. Many teams use internal package names — a private helper library, an internal SDK — that live in a private registry. If the build is configured to check both the private registry and a public one, and it resolves by picking the highest version number it can find, an attacker who learns your internal package name can publish a package with that same name to the public registry at an absurdly high version. On the next build, the resolver dutifully picks the highest version — the attacker's — and runs their code inside your pipeline. No breach of your systems was required. The build reached out and grabbed the malicious package on its own.

Why this is a build-pipeline problem, not just a dependency problem

Dependency confusion is a cousin of the software supply chain risks a lean team already worries about, but it has a sharper edge: it turns your automated build into the delivery mechanism. The malicious code does not run on a developer laptop after a careless click — it runs in the pipeline, often with credentials, network access, and the ability to sign or publish artifacts. That is the same reason typosquatting and namespace attacks are so effective: the trust boundary is not a person deciding to install something, it is a machine following resolution rules at high speed.

  • Typosquatting publishes a package one keystroke off a popular name — reqeusts for requests — and waits for a fat-fingered dependency line.
  • Dependency confusion does not need a typo at all. It exploits the resolver preferring a public higher version over your private lower one, so your correctly spelled internal name pulls the attacker's package.
  • Namespace and account takeover on a registry lets an attacker push a malicious version of a real, trusted package under its legitimate name.

All three share a root cause: the build trusts a name and a version number, and names and version numbers are cheap for an attacker to control.

Close the resolution gap

The fixes are configuration discipline more than tooling, and a lean team can finish them:

  • Scope and namespace your internal packages so the private name cannot be claimed on a public registry — a reserved scope means the public registry has no matching package for the resolver to prefer.
  • Pin versions and lock the tree. A committed lockfile with integrity hashes means the build installs the exact artifact you resolved and reviewed, not whatever higher version appeared overnight. This is the same discipline as a rigorous patch cadence: deliberate updates, not silent drift.
  • Configure the resolver so private beats public, or route all installs through a single internal proxy that decides source explicitly instead of letting the client merge registries and pick the highest number.
  • Restrict which registries the build can reach at all. A build that can only fetch from an approved, curated source cannot be tricked into reaching out to a public one — the same egress-restriction logic applied to the pipeline.

Watch the dependency tree as a living thing

Configuration closes the door; monitoring tells you when someone tries the handle. Your dependency graph is an asset inventory that changes on every build, and it deserves the same standing watch:

  • Maintain an SBOM and diff it. A software bill of materials that lists every package and version turns "what are we actually running" from a guess into a fact — and a diff that shows a new source or an implausible version jump is an early warning.
  • Alert on new or unexpected sources. A package that suddenly resolves from a public registry when it always came from private is exactly the signal dependency confusion produces.
  • Treat a suspicious resolution as a finding. A version that leapt, a source that changed, a name that now exists on a public registry it should not — each is a finding with an owner and a clock, ranked by what that build can reach under the same exposure-first triage as everything else.

The evidence it produces

An assessor reviewing how you manage third-party code will ask how you know what your build installs and how you prevent it from installing something you did not intend. A current SBOM, a locked dependency tree, and a record that your registry configuration is enforced are exactly that answer — and they feed straight into continuous monitoring and evidence collection instead of being reconstructed under deadline.

One honest caveat: a platform can inventory your dependencies, maintain and diff an SBOM, and raise a tracked finding when a package resolves from an unexpected source or jumps to an implausible version — it organizes, watches, and proves the work. It does not configure your registries, pin your lockfiles, or scope your namespaces for you, and it does not grant or guarantee any certification; the resolver configuration, the version pinning, and the remediation are operational steps your team owns, and which obligations a compromised dependency triggers is a question for counsel.

Your build makes a trust decision on every install, and dependency confusion is an attacker learning to make that decision wrong — no breach required, just a name they guessed and a version number they inflated. Scope your internal packages, pin the tree, make private beat public, and watch the dependency graph for the source that changed. The build should install only what you meant, and be able to prove it did.