I remember reading Alex Birsan's disclosure when it dropped in February 2021 and immediately checking our own systems. Within an hour, I found three packages that were vulnerable. We weren't special—nearly every organization I've audited since then has had similar exposure.
The attack is elegant in its simplicity. Package managers like npm, pip, and others typically check multiple sources when resolving dependencies. If your application depends on a package called internal-auth-library and that package exists on both your private registry and the public npm registry, the package manager has to decide which one to use.
The tiebreaker is usually the version number. Higher version wins. So if an attacker publishes internal-auth-library version 9999.0.0 to the public registry, and your private version is 1.2.3, guess which one gets installed?
The Information Leak Problem
The attack requires knowing the names of your private packages. You might think this information would be difficult for attackers to obtain, but it's remarkably easy to find.
Package names often leak through GitHub repositories where developers accidentally commit package.json or requirements.txt files with private package references. They appear in error messages on public-facing applications. They show up in JavaScript source maps. They're discussed in job postings and conference talks. Some companies even use predictable naming schemes that make guessing trivial.
Once an attacker has a list of potential package names, they simply publish packages with those names to public registries and wait. If any build system pulls one of their packages, they typically receive a callback with information about the compromised environment—hostnames, usernames, environment variables, the works.
The Problem With Most Mitigations
The standard advice for preventing dependency confusion is to use scoped packages (like @company/internal-auth-library instead of just internal-auth-library) or to configure your package manager to only use private registries for specific packages.
Both approaches help, but neither is bulletproof. Scoped packages can still be confused if an attacker registers the scope before you do. Configuration-based solutions require every developer and CI system to be configured identically, which rarely happens in practice.
I've seen organizations implement what they thought were comprehensive fixes, only to discover months later that one legacy Jenkins server was still pulling from public registries because nobody remembered to update its configuration.
A Better Approach
The most reliable defense is to route all package installations through a proxy that you control. Rather than allowing build systems to connect directly to public registries, they connect to your internal proxy, which then fetches packages from approved sources based on explicit allowlists.
This approach has several advantages. It gives you a single point of control for all package installations across your organization. It allows you to implement soak time policies consistently. And it means a misconfigured CI server can't accidentally bypass your security controls—if it can install packages at all, those packages have to go through the proxy.
The trade-off is operational complexity. You're now running infrastructure that sits in the critical path for all software builds. If it goes down, development stops. That's a real cost that has to be weighed against the risk.
But given how many organizations have been compromised through dependency confusion—and how many more have quietly cleaned up incidents without public disclosure—it's a cost that's increasingly difficult to avoid.