Dependency Confusion¶
The Lesson: If your internal package names exist on public registries, someone else can publish malicious code under those names.
Elegant and Terrible
This attack was elegant. Birsan didn't break in—he exploited assumptions nobody questioned. Package managers checking multiple registries? Feature. Higher versions winning? Feature. Internal names not protected? Oversight nobody noticed. He earned $130,000 by asking "what if?" about default behavior. That's the worst kind of vulnerability: the one that's not a bug.
What It Is¶
Dependency confusion exploits how package managers resolve names. When you request a package, the manager typically checks multiple sources: your private registry (if configured) and public registries like npm or PyPI.
If an attacker knows the name of your internal package—and that name doesn't exist publicly—they can register it on the public registry with a higher version number. Many package managers will prefer the "newer" public version over your internal one.
Your build now installs the attacker's code instead of yours.
The Discovery¶
In February 2021, security researcher Alex Birsan published research demonstrating this attack against major tech companies.1 By discovering internal package names through various reconnaissance techniques, he was able to get his code executed on internal systems at:
- Apple
- Microsoft
- PayPal
- Shopify
- Netflix
- Tesla
- Uber
- And dozens more
He earned over $130,000 in bug bounties from companies whose build systems executed his proof-of-concept code.
How It Works¶
Discovery Phase¶
Attackers find internal package names through:
Public code leaks: Internal package names in public GitHub repos (accidentally committed package.json, requirements.txt, etc.)
Error messages: Stack traces that mention package names.
Documentation: Internal wikis or docs accidentally exposed.
JavaScript source maps: Bundled JavaScript that reveals import paths.
Job postings: "Experience with our internal X library required."
Attack Phase¶
-
Register the name publicly. If
@mycompany/internal-authis the internal package, registerinternal-authon npm (without the scope). -
Use a high version number. Version 99.0.0 will be "newer" than any internal version.
-
Add payload. Include code that executes during installation (postinstall scripts) or import.
-
Wait. When the target's build system runs, it may resolve to your public package.
Why It Succeeds¶
Default resolver behavior: Many package managers check public registries by default, even when private registries are configured.
Version comparison: Higher version numbers are considered "better" without regard to source.
Namespace confusion: Internal packages often don't use scoped names (@company/package) that would prevent public collision.
CI/CD automation: Build systems run unattended, executing whatever the package manager resolves.
Affected Ecosystems¶
The attack works differently across package managers:
npm: Vulnerable if internal packages don't use scoped names (@scope/package). Scoped packages are protected—only the scope owner can publish to them.
Python/pip: Particularly vulnerable. pip's --extra-index-url flag adds sources but doesn't prioritize them. A higher version on PyPI can win over a private index.
Ruby/Bundler: Less vulnerable by default, but custom configurations can be exploited.
Go: Less vulnerable due to the module path system (packages are namespaced by repository URL).
Prevention¶
Use scoped/namespaced packages¶
npm: Use organization scopes: @mycompany/package-name
Python: Use unique prefixes unlikely to collide: mycompany-internal-package
Go: Use your organization's domain in module paths
Configure package managers correctly¶
Python: Use --index-url (replaces PyPI) instead of --extra-index-url (adds to PyPI). Or use explicit source mapping.
# pip.conf - SAFER: replaces public PyPI
[global]
index-url = https://private.registry.example.com/simple/
npm: Configure scopes explicitly:
Pre-register public names¶
If you have internal packages with generic names, consider registering placeholder packages on public registries. Even an empty package prevents attackers from claiming the name.
Monitor for squatting¶
Watch public registries for packages matching your internal names. Some security tools automate this monitoring.
Network controls¶
Block or alert on unexpected outbound connections from build systems. If your build suddenly reaches out to PyPI when it should only talk to your private registry, that's a red flag.
Unquestioned Assumptions
Dependency confusion is clever because it exploits trust assumptions nobody questioned.
Package managers were designed when "installing a package" meant downloading from one central place. Private registries were bolted on later. The resolution logic wasn't designed for a world where multiple sources might have the same package name with different contents.
Birsan's research was a wake-up call. Suddenly, every organization with internal packages had to ask: could someone guess these names? Is our resolver configured correctly? What happens if there's a version conflict?
The fix is straightforward—use namespaces, configure your package manager correctly, don't trust default behavior. But lots of organizations learned these lessons the hard way. If you have internal packages, audit your resolver configuration. Today. Before someone else discovers what names you're using.
Key Defenses¶
| Defense | npm | Python | Go |
|---|---|---|---|
| Namespacing | Use @scope/name | Use unique prefixes | Module path is namespaced |
| Registry config | Scope-specific registries | --index-url not --extra-index-url | GOPROXY settings |
| Placeholder packages | Pre-register names | Pre-register names | N/A (path-based) |
| Version pinning | Lock files | Lock files | go.sum |