Dependency Confusion Attack: How It Works and How to Stop It
A dependency confusion attack exploits how package managers resolve names: when your build references an internal package (say, @acme/auth-utils) that also exists on a public registry like npm or PyPI, the tool may fetch the attacker's public version if it carries a higher version number. The attacker never breaks in — your own CI pipeline downloads and executes their code. It works because most default resolvers merge private and public sources and prefer the highest version, regardless of origin.
How a dependency confusion attack actually works
Dependency confusion is one of the three most common npm supply-chain attack patterns, alongside typosquatting (registering expres to catch typos of express) and malicious post-install scripts that exfiltrate credentials during install. What makes confusion distinct is that it targets names you already trust.
The attack, first demonstrated at scale by researcher Alex Birsan in 2021, follows a simple recipe:
- Discover internal package names. These leak in public GitHub repos, JavaScript source maps,
package.jsonfiles, or Dockerfiles. - Register the same name on the public registry. If
@acme/auth-utilsis only in your private registry but the scope isn't reserved on npm, an attacker can claim it. - Publish a higher version. Your internal package is
1.2.0; the attacker publishes99.0.0. The resolver picks the "newest." - Run code on install. A
preinstallorpostinstallscript fires the moment your build pulls the package, giving the attacker code execution inside CI.
Birsan used exactly this technique to get code running inside Apple, Microsoft, PayPal, and dozens of other companies — turning it into one of the highest-earning dependency confusion bug bounty categories that year.
A concrete example: npm and Python
In npm, imagine your internal registry hosts @acme/billing-client. Your .npmrc configures the public registry as a fallback. An attacker publishes @acme/billing-client@100.0.0 to public npm with this package.json:
"scripts": { "postinstall": "node exfil.js" }exfil.jsreads environment variables (AWS keys, npm tokens) and POSTs them to an attacker server.
The same flaw affects a dependency confusion attack in Python. If pip install runs with --extra-index-url pointing at your private index plus PyPI, pip considers packages from both and picks the highest version. A malicious internal-analytics package on public PyPI overrides your private one, and its setup.py executes arbitrary code during install. This is why mixing indices without pinning is dangerous across ecosystems, not just npm.
Where confusion sits among supply chain attacks
Dependency confusion is one flavor of a broader problem. Real-world software supply chain attacks include:
- SolarWinds (2020) — attackers compromised the build system and shipped a trojaned update to ~18,000 customers.
- event-stream (2018) — a maintainer handed off a popular npm package; the new owner injected code targeting a bitcoin wallet.
- Codecov (2021) — a modified CI script leaked environment secrets from thousands of builds.
- Dependency confusion (2021+) — name-collision attacks against internal packages, still actively exploited.
The common thread: you inherit the trust of everything you install. That's why Executive Order 14028 (May 2021) now requires federal agencies and their suppliers to maintain a Software Bill of Materials (SBOM) for all software sold to the government — so organizations can actually enumerate what they depend on and detect substitution.
How to prevent dependency confusion
Fixes are concrete and layered — apply more than one:
- Reserve your scopes. Register your org's npm scope (
@acme) and PyPI package names publicly so attackers can't claim them, even if you don't publish there. - Scope resolution to one source. In npm, pin internal scopes to your private registry in
.npmrc:@acme:registry=https://registry.internal/. Don't let scoped packages fall back to public. - Use a controlled proxy. Route all installs through Artifactory, Nexus, or a similar repository that resolves internal names locally and never substitutes a public package for a private name.
- Avoid mixed indices in Python. Replace
--extra-index-urlwith a single trusted index, and pin exact versions and hashes withpip install --require-hashes. - Lock versions and verify integrity. Commit lockfiles (
package-lock.json
Ready to practise the decisions these articles describe?
Run a free War Room →