Dependency Confusion Attack: npm & PyPI Defenses
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. It is one of the best-known package supply-chain attack patterns, alongside typosquatting and 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, who published his research in February 2021. He collected internal package names from package.json files exposed in public JavaScript and repositories, then published matching packages to npm, PyPI, and RubyGems with benign phone-home code. Those packages ran inside the networks of more than 35 companies, including Apple, Microsoft and PayPal, earning him over $130,000 in bug bounties 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.
Useful next, if this was relevant to you
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) put SBOMs on the map: it directed NIST to publish secure-software guidance — including providing purchasers an SBOM — and it led to OMB memo M-22-18, which requires vendors selling software to US federal agencies to attest to secure development practices (agencies can also ask for an SBOM). It governs software sold to the federal government, not private companies in general, but the lesson travels: you cannot defend a supply chain you cannot enumerate.
How to prevent dependency confusion — a concrete checklist
Prioritize these controls in order:
- Claim your names publicly. Register your organization's scope on npm (for example the
@acmeorg) so no one else can publish under it, and, where the registry's policy allows, reserve unscoped internal names on PyPI. npm's policies discourage name-squatting placeholders, which is another reason to move internal npm code under a scope. - Use scoped packages. On npm, publish internal code under a scope like
@acme/and map that scope to your private feed in.npmrc(@acme:registry=https://npm.internal.example/), so it never resolves from the public registry. A scoped name cannot be squatted by someone who doesn't control 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, or devpi for Python) that mirrors approved public packages, rather than mixing private and public indexes at resolution time. For pip, that means
--index-urlpointing at the proxy and no--extra-index-url. Check how the proxy itself resolves a name that exists in both its local and remote repositories: JFrog Artifactory has a priority-resolution setting for local repositories, and Sonatype's Repository Firewall offers namespace-confusion protection. - Generate an SBOM. Produce a CycloneDX or SPDX SBOM per build so you know exactly what shipped — increasingly requested in US federal procurement and useful everywhere.
To vet the public packages your builds pull in, PlayCISO's NPM Package Scanner scores npm packages against 40+ risk policies, including malicious install scripts, ownership transfers and typosquats. It can't see your private registry, so the scoping and registry steps above are still yours to do.
Frequently asked questions
What is a dependency confusion attack?
An attacker publishes a package to a public registry such as npm or PyPI under the same name as one of your internal packages, with a higher version number. If your package manager checks both your private registry and the public one, it can pick the attacker's higher version and run their code in your build.
How do you prevent dependency confusion in npm?
Put internal packages under a scope you own (for example @acme), register that scope on the public npm registry, map the scope to your private registry in .npmrc, commit lockfiles with integrity hashes, and route all installs through one curated proxy.
How do you prevent dependency confusion in pip?
Don't use --extra-index-url to combine a private index with PyPI, because pip picks the highest version from either. Point --index-url at a single curated proxy that serves both your internal packages and approved PyPI packages, and pin versions with hashes.