A build system is a machine that turns names into code. When your build says npm install example-internal-library, the package manager takes that string, asks a registry where to find it, and downloads whatever comes back. The entire transaction rests on a single assumption that nobody ever states out loud: the name maps to the code you meant. The dependency confusion attack exists because that assumption is routinely false.
In early 2021 a security researcher demonstrated the attack against dozens of major technology companies by publishing packages to public registries with names that matched those companies' private, internal-only packages. The names were real to the companies. They simply were not real to the public registries — until the researcher made them real. When the companies' build systems queried a public registry for a package that only existed internally, the public registry happily answered with the attacker's copy. In several cases, the internal systems silently downloaded the attacker's code.
Why the Attack Works
Dependency confusion is not a bug in any single tool. It is a structural property of how package ecosystems are wired:
- Names are global, ownership is not enforced. Package registries are essentially global namespaces. Anybody can register any name that isn't already taken, and nothing verifies that a name is "yours" because your company happens to use it internally.
- Package managers look in more than one place. A typical configuration checks an internal registry first, a public mirror second, and fails over quietly. When an internal package name is not found locally, the resolver falls through to the public registry — and the attacker's package has been waiting there with a very high version number.
- Version numbers beat names. Most resolution logic favors the highest available version. The attacker simply publishes version 999.0.0 of the internal-looking name, and the resolver treats it as the obvious choice.
- Typosquatting is the cheap cousin. Registering requsts instead of requests, or lodahs instead of lodash, catches developers who type fast and look rarely. It is not as surgical as dependency confusion, but it needs no inside knowledge.
The Anatomy of a Confused Install
Thought experiment — the build that downloaded the attacker
Your organization maintains an internal package, auth-middleware, used by every service in the company and hosted only on a private registry. An attacker guesses the name — easily, because it appears in your public API docs — and publishes auth-middleware@999.0.0 to the public npm registry with a payload that steals environment variables and uploads them to a server the attacker controls.
Six months later, a new hire runs a fresh npm install in a service whose lock file was generated before the attacker's package existed. The lock file has no pinned hash for auth-middleware, because at the time it was generated the package only existed internally. The resolver checks the private registry, doesn't find it, and follows the configured fallback to the public registry, where the attacker's 999.0.0 looks like the newest, most legitimate version of the dependency the code clearly expects.
The build succeeds. The tests pass. The service deploys. The attacker now has every secret any environment variable could hold — cloud keys, database credentials, tokens — from inside the perimeter, with a fully green build and an audit trail that shows a perfectly ordinary dependency install.
How Attackers Find the Names
Dependency confusion is often described as requiring inside knowledge, but the knowledge is easier to obtain than the name implies. The names of internal packages leak through a dozen routine channels: public package manifests that were never meant to be public, code snippets in job postings, conference slides, open-source repositories that accidentally include an internal package.json or requirements.txt, and the mirrors that companies used to publish internally before they had a private registry. An attacker does not need to guess a name. They need to search for it.
The scan for claimable names is now automated. Researchers and attackers alike run tools that harvest public code for dependencies that do not resolve publicly, then check whether those names are unclaimed on public registries. What took the 2021 research weeks by hand is now a script. The names that were novel once are now systematically discoverable, which is precisely why the defense has to be structural rather than obscurity-based — you cannot hide a name that your own public artifact listings reveal.
The Variants Beyond the Classic
Dependency confusion has a family of variants, each tuned to a different blind spot. The same confusion can be achieved by publishing a malicious package with a name that exists only in a niche namespace, or by exploiting resolver behavior that prefers certain registries over others. The attack also appears in reverse: a public registry that is told, incorrectly, to trust an internal mirror, so an attacker with any access to the mirror controls what "public" resolves to. The unifying thread is not the specific mechanism. It is the resolver's willingness to combine claims from multiple authorities and treat the combination as a single, trusted result.
Why It Keeps Succeeding
The attack was demonstrated loudly in 2021, and packages continue to be discovered in the wild using variants of it. The reasons are institutional as much as technical:
- Private registries are the exception, not the default. Many teams still rely on public registries with internal packages mirror-published to them — sometimes to the public registry itself, where the name is now genuinely claimable by anyone.
- Lock files encode history, not intent. A lock file pins what was installed once, but a missing hash or a name that didn't exist at lock time opens a hole for future resolution.
- The security review happens after the install. Dependency scanning is usually a post-hoc check that runs on the final dependency tree. It can flag a malicious package, but it runs after the build consumed it — and post-hoc detection is a weak control when the payload executes at install time.
- Attackers iterate quietly. The loudest versions of the attack were demos. Real campaigns use subtle name collisions, moderate version numbers, and payloads that don't trigger on the first machine — waiting for a production environment where the value is higher.
Checking Whether Your Build Is Vulnerable
You can assess your exposure in an afternoon without any tooling:
| Test | What it reveals |
|---|---|
| Search the public registry for every name your code imports | Confirms whether your "internal" package names are claimable by outsiders |
| Check resolver configuration for silent fallback to public registries | Finds the fail-open behavior the attack depends on |
| Inspect lock files for missing integrity hashes | Identifies dependencies that could be resolved differently on the next install |
| Build from a clean checkout with a fresh dependency cache | Reproduces what a new machine — or an attacker's trigger — would actually install |
| Compare what's in your private registry against what your manifest declares | Reveals names that exist in manifests but have no trusted source |
The Defenses That Actually Close It
Dependency confusion has well-established defenses, and they share a theme: make resolution deterministic and explicit.
- Pin integrity hashes. Lock files with cryptographic hashes for every resolved artifact mean the resolver cannot silently substitute a different package, whatever registry answers.
- Eliminate silent fallback. Configure package managers to fail the build when a package is missing from the trusted registry instead of reaching out to a public one. A build that fails loudly is a feature.
- Publish internal packages to a registry you control. If a name exists only on your private registry, it cannot also exist — claimable — on a public one. Keep the namespaces separate and never mirror internal names outward.
- Reserve the names anyway. As defense in depth, register your internal package names on public registries even if you never intend to publish them, so an attacker cannot claim them later. This is the "squat your own namespace" play, and it works.
- Scan at install time, not just after. Policy engines that evaluate packages as they are resolved, rather than after the fact, can reject a malicious artifact before its code runs.
- Watch for the impossible version. Alert on resolution to a version that differs wildly from what your manifests and histories would suggest.
Key Takeaways
- Dependency confusion works because names are global strings and package managers fail open by default.
- The attack needs no compromise of your infrastructure — only a name your code already trusts.
- Typosquatting is the blunt version; dependency confusion is the surgical version aimed at real internal names.
- Integrity hashes, no-fallback resolution, private registries, and name reservation close the practical gaps.
- Your build system's most dangerous behavior is its willingness to trust a stranger who answers first.
The uncomfortable truth of the package ecosystem is that a name is a promise with no binding. Every build you run is a small act of trust in whoever happened to register the names your code uses. Dependency confusion demonstrated that this trust can be harvested at scale, for the cost of a few package registrations. The fix is not to stop trusting your dependencies — it is to make sure the trust is placed in a registry, a hash, and a lockfile that can hold a promise, rather than in a namespace anyone can claim.