๐ŸŽ‰ New here? Use code WELCOME10 for 10% off any plan at checkout
All posts

Dependency Confusion Attacks: How They Work and How to Stop Them

September 25, 2026 ยท PlayCISO

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.json file, 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.txt lists acme-auth-utils==1.2.0.
  • An attacker publishes acme-auth-utils 99.0.0 to public PyPI.
  • If your pip config uses --extra-index-url to add PyPI alongside your private index, pip may prefer the public 99.0.0 โ€” because --extra-index-url gives 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-scripts where possible to neutralize post-install exfiltration.
  • Use version pinning and lockfiles. package-lock.json and pinned requirements.txt with hashes prevent silent upgrades to a rogue 99.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 โ†’