Dependency Confusion Attacks: How They Work and How to Stop Them
A dependency confusion attack exploits how package managers resolve names: when your build tool looks up a dependency, it may pull a malicious package from a public registry (like npm or PyPI) instead of your intended internal package โ simply because the attacker published a higher version number under the same name. Security researcher Alex Birsan demonstrated this in 2021 by breaching Apple, Microsoft, and dozens of other companies using it, earning over $130,000 in bug bounties. The fix is to explicitly scope and pin where each dependency comes from.
How a dependency confusion attack actually works
Most internal packages live in a private registry. But package managers often check both private and public registries, and by default they resolve to the highest available version regardless of source. The attack unfolds in three steps:
- Reconnaissance: An attacker finds the name of an internal package โ often leaked in a public GitHub repo, a
package.jsonfile, or an error log. - Squatting: They publish a package with that exact name to the public registry (npm, PyPI, RubyGems) with a very high version number, e.g.
9.9.9. - Execution: When a developer or CI pipeline runs an install, the package manager sees the public version is "newer" and pulls the attacker's code โ frequently including a post-install script that exfiltrates credentials or environment variables.
Dependency confusion is one of the three most common npm supply-chain attack patterns, alongside typosquatting (registering reqeusts to catch typos of requests) and malicious post-install scripts. All three abuse the trust developers place in automatic dependency resolution.
A concrete example in Python and npm
Say your company uses an internal Python package called acme-auth-utils, installed from a private index:
- Your
requirements.txtlistsacme-auth-utils==1.2.0. - An attacker publishes
acme-auth-utils 99.0.0to public PyPI. - If your pip config uses
--extra-index-urlto add PyPI alongside your private index, pip may prefer the public99.0.0โ because--extra-index-urlgives no priority guarantee and pip resolves to the highest version across all indexes.
The npm version is identical in spirit: an internal package @acme/config published unscoped as acme-config on public npm, with a version bump, gets pulled during npm install. The malicious package's preinstall or postinstall hook then runs arbitrary code on your build machine โ the same class of software supply chain attack that led to widespread SBOM requirements.
How this fits the broader supply chain threat
Dependency confusion sits alongside other well-documented supply chain attacks: SolarWinds (compromised build pipeline, 2020), the event-stream npm incident (malicious code injected into a popular package to steal Bitcoin wallets), and Codecov's bash uploader compromise. What they share is that the attacker never touches your code directly โ they poison something you trust and pull automatically.
This is why Executive Order 14028 (May 2021) requires federal agencies and their suppliers to maintain a Software Bill of Materials (SBOM) for all software sold to the government. An SBOM won't prevent dependency confusion on its own, but it gives you an inventory of every dependency and its declared source โ the prerequisite for detecting when a package suddenly resolves to an unexpected registry or version.
Five controls that actually stop it
- Scope your packages. On npm, publish and consume internal packages under a scope (
@acme/*) and map that scope to your private registry in.npmrc. A public attacker can't claim your scope. - Pin the source, not just the version. For pip, prefer
--index-url(single source) over--extra-index-url, or use a proxy repository like Artifactory or Nexus that resolves internal names first and never falls through to public for known internal names. - Reserve your names publicly. Register placeholder packages of your internal names on public npm and PyPI so attackers can't squat them.
- Disable install scripts in CI. Run
npm install --ignore-scriptswhere possible to neutralize post-install exfiltration. - Use version pinning and lockfiles.
package-lock.jsonand pinnedrequirements.txtwith hashes prevent silent upgrades to a rogue99.0.0.
Prioritize scoping and proxy configuration first โ they eliminate the resolution ambiguity that makes the attack possible, rather than just reducing blast radius.
If you want to check whether your own repositories leak internal package names or reference dependencies vulnerable to confusion, try PlayCISO's free NPM Supply Chain Risk Scanner โ it flags unscoped internal pack
Ready to practise the decisions these articles describe?
Run a free War Room โ