concept

Dependency Confusion

also called Internal Package Name Hijack, Registry Resolution Confusion

A build resolving an internal package name from a public registry because the package manager prefers the highest version across all configured sources - an anti-pattern in resolution configuration rather than a flaw in any package.

supply-chainpackage-registrybuild-securitynamespacesresolution

A build file lists @acme/billing-core, which exists only in your private registry. The build machine is configured with both the private registry and the public one, because somebody needed a public dependency once and adding a second source was the quick fix.

Most package managers resolve a name across every configured source and prefer the highest version they find, not the most trusted source they find. A public package with the same name and a higher version number wins, and the build installs it without warning, because nothing unusual happened from the resolver's point of view.

The names are not secret. They appear in public repositories, published front-end bundles, stack traces and build logs pasted into issue trackers. Treating an internal package name as a secret is the mistake underneath the mechanism.

Why it matters

The install step of a package manager runs code, and it runs in the build environment, which usually holds deployment credentials, signing keys and network reach into internal systems. A build runner is the highest-privilege machine in most estates and the least reviewed, so this is a short path from a naming accident to production.

It also defeats the controls people believe cover it. Dependency scanning checks known vulnerabilities and this package has no advisory. Artifact signing checks that your pipeline produced the artifact, and it did. The defect sits in resolution, upstream of both.

Implementation patterns

  • Own the namespace publicly. Reserve your scope or prefix on every public registry you resolve from, so the name cannot be claimed by anyone else. Highest value per unit of effort on this list, and under 30 minutes per namespace.
  • One resolution source. Point every client at the private registry and have it proxy the public one rather than listing both, so resolution order has no ambiguity to exploit.
  • Lockfiles with integrity hashes enforced in continuous integration. A build that regenerates its lockfile has no lockfile.
  • Block install-time scripts by default in the build image and allow them per package. This is what turns a resolution mistake into a failed build rather than code execution.
  • Alert on a first-seen package name resolving from the public proxy. New names are rare in a mature repository, so the alert rate stays low enough to act on.

Industry example

In February 2021 the security researcher Alex Birsan published work demonstrating the technique at scale. By harvesting internal package names from public repositories and build error messages and publishing same-named packages with high version numbers to npm, PyPI and RubyGems, he reached the internal build systems of more than 35 organisations, several among the largest technology companies, and was paid over $130,000 in bug bounties. Registries and several package managers changed guidance afterwards, and namespace reservation became standard advice.

Failure scenarios

  • A new team adds the public registry to a client configuration to install one tool, silently widening resolution for everything.
  • A monorepo with mixed ecosystems, where the Node and Python configurations are hardened and a third is not.
  • A private registry outage leading someone to remove it from the configuration to unblock a release, leaving public-only resolution behind.
  • A renamed internal package whose old name is now unclaimed and still referenced by a branch that still builds, or a container image build that resolves dependencies outside the hardened pipeline.

Trade-offs

A proxying private registry removes the ambiguity and becomes a build-time single point of failure: when it is down, nothing builds. That needs a cache, an availability target and a documented bypass tighter than "add the public registry back". Blocking install scripts breaks a real minority of packages and costs a per-package allowlist somebody maintains. Namespace reservation is nearly free and covers only names you thought to reserve, so it belongs in the repository-creation workflow rather than in a one-off sweep.

When not to use it

The heavy controls are not proportionate at every size. A team with one repository, one ecosystem and a single registry already has unambiguous resolution, and adding a proxy layer buys a new outage mode. The part always worth doing is reserving the namespace and enforcing the lockfile, because both are one-time and neither adds an operational dependency. Install-script blocking is where to stop if the estate is small, since the maintenance burden lands on the few people who would have noticed the problem anyway.

Interview question

Q: "Your scanner reports zero vulnerable dependencies and your pipeline signs every artifact. Explain how an attacker still gets code execution in your build, and name the three controls you would fund first."

What a strong answer covers: that resolution happens before scanning and signing, so both are blind to it; that internal names are public information rather than secrets; the three controls being namespace reservation, single-source resolution through a proxy, and enforced lockfiles with install scripts off by default; and that the build runner's credential scope is the real blast radius, so reducing what the runner can reach is worth as much as the resolution fix.

Quick check

Quiz: Why does artifact signing not prevent dependency confusion? Answer: the wrong package is consumed before the artifact exists, so the pipeline signs an artifact it genuinely built.

Flashcard: Cheapest control against dependency confusion? Reserving your package namespace on every public registry you resolve from.