Dependency Confusion Attack: How It Works and How to Stop It
A dependency confusion attack exploits how package managers resolve dependencies: an attacker publishes a malicious package to a public registry (like npm or PyPI) using the same name as one of your internal, private packages. When your build tool sees two packages with the same name, it often defaults to the one with the higher version number โ which the attacker sets artificially high โ and pulls their code straight into your build. This is one of the three most common npm supply-chain attack patterns, alongside typosquatting and post-install scripts that exfiltrate credentials.
What a dependency actually is โ and why confusion happens
A dependency is any external code your application relies on to run. When you write import requests in Python or require('lodash') in Node, you're declaring a dependency your package manager must fetch and install. Modern apps have hundreds of them, most pulled automatically from public registries.
The confusion arises from namespace collision. Large organizations build internal packages โ say acme-internal-auth โ hosted on a private registry. If the package manager is configured to check both the private registry and the public one, and no public package with that name exists, an attacker can register acme-internal-auth publicly. Because the public copy is version 99.0.0 and your internal copy is 1.4.2, the resolver grabs the attacker's version. Nothing was misspelled and no one was phished โ the system worked exactly as designed, against you.
A real dependency confusion attack example
The technique was proven at scale by security researcher Alex Birsan in 2021. By scraping internal package names from leaked package.json files, error logs, and public GitHub repos, he published matching packages to npm, PyPI, and RubyGems with benign phone-home code. Those packages executed inside the build systems of dozens of major companies, earning six figures in bug bounty payouts and turning "dependency confusion" into a recognized attack class overnight.
A Python dependency confusion variant follows the same logic with pip. If you install internal packages using --extra-index-url pointed at both a private index and PyPI, pip will consider both sources and select the highest version โ regardless of which index it came from. An attacker who learns an internal module name (corp-data-utils, for example) can publish it on public PyPI and have it silently override your private build. The fix in pip's world is to avoid --extra-index-url in favor of a curated internal index that proxies public packages, or to pin exact versions and hashes.
Where this fits in the supply chain attack landscape
Dependency confusion is one flavor of software supply chain attack. Others you should know:
- Typosquatting โ registering
reqeustsorloadashto catch developers who fat-finger a package name. - Post-install script abuse โ malicious code in npm's
postinstallhook that runs automatically onnpm installand exfiltrates credentials, SSH keys, or environment variables. - Compromised maintainer accounts โ attackers hijack a legitimate popular package and push a malicious update (as seen with event-stream and, more recently, several npm crypto-stealer campaigns).
- Build-system compromise โ the SolarWinds pattern, where the attacker poisons the artifact during the build rather than in a dependency.
What ties them together is implicit trust: your build automatically executes third-party code you never reviewed. That's 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 โ you cannot defend a supply chain you cannot enumerate.
How to prevent dependency confusion โ a concrete checklist
Prioritize these controls in order:
- Claim your internal names publicly. Register your private package names (and common scopes) on npm and PyPI as empty placeholders so attackers can't. This is the single fastest defensive move.
- Use scoped packages. On npm, publish internal code under a private scope like
@acme/and configure the registry so that scope resolves only to your private feed. A scoped name cannot be squatted without controlling the scope. - Pin versions and verify integrity. Commit lockfiles (
package-lock.json,poetry.lock) with integrity hashes so a substituted package fails verification. - Configure a single, curated registry. Point builds at one internal proxy (Artifactory, Nexus, Verdaccio) that mirrors approved public packages, rather than mixing private and public indexes at resolution time.
- Generate an SBOM. Produce a CycloneDX or SPDX SBOM per build so you know exactly what shipped โ required for federal work and useful everywhere.
If you want to check whether your own internal package names are exposed or squattable before an attacker does, PlayCISO's free NPM Supply Chain Risk Scanner flags unclaimed names, unscoped internal packages, and
Ready to practise the decisions these articles describe?
Run a free War Room โ