Dependency Confusion Attacks: How They Work and How to Stop Them at Build Time

Your software build imports external packages based on the name of the package. When your package manager sees a dependency called internal-auth-utils, it searches the configured package sources for a package with the dependency name. Dependency confusion leverages that process to perform package substitution. An attacker doesn’t need to breach your network or steal a credential. They simply publish a package to a public registry using the same name as one of your private dependencies with a newer version number, and your build pulls the substituted package instead of yours.

This threat isn’t just theoretical; it is real and documented. In 2021, security researcher Alex Birsan used this single trick to achieve confirmed code execution inside Apple, Microsoft, Uber, and more than 30 other organizations. In May of 2026, Microsoft documented a dependency confusion campaign involving 45 malicious npm packages across three maintainer accounts, including an initial 33-package wave followed by 12 additional packages the next day. These attacks exploited dependency confusion to profile developer and build environments. They abused package-resolution logic, not a conventional software bug that can simply be patched.

What Is a Dependency Confusion Attack?

A dependency confusion attack exploits how package managers resolve a name when that name could come from more than one source. Typically, the exploit involves private internal registries and public ones like npm, PyPI, or RubyGems. When private and public package sources are mixed on the same resolution path, the package manager or registry proxy may compare candidates across both sources and select the highest version that satisfies the dependency. That is what creates the confusion: a public impostor can outrank the intended private package.

You’ll also see it called dependency substitution or namespace confusion, and OWASP catalogs it under CICD-SEC-03, Dependency Chain Abuse. Crucially, the attack needs no credentials and no coded exploit. The attacker only needs to know the name of one of your internal packages.

How the Attack Unfolds

Reconnaissance on internal package names

Internal package names leak constantly. They show up in public JavaScript bundles, in package.json or requirements.txt files committed to public repositories, in stack traces and error messages, and in cached build artifacts or published SBOMs. The attacker doesn’t need access to anything beyond the package name.

Claiming the package name on a public registry

Because that internal name was never registered publicly, it’s available. The attacker registers it on the public registry, taking ownership of a name your build already pulls in. Reserving internal package names in public repositories can be used as a core defense against this type of exploit.

Publishing an impostor with a higher version number

The attacker will typically publish their package with an absurdly high version (like 9.9.9 or 999.0.0), so that if the build is comparing public and private sources, the public copy looks newer and wins.

The package manager pulls the public version into the build

When a build is configured to consult both a private and a public source, the resolver compares versions across them and picks the highest. With pip, the –extra-index-url flag can create exactly this risk by allowing pip to search an additional package index where a public package may be chosen instead of the intended private one. With npm, unscoped internal package names, missing .npmrc rules, or registry proxy behavior can create a similar outcome: the build may resolve a private dependency name from the public npm registry. This is the moment of confusion: the build fetches the attacker’s package, believing it’s yours.

Malicious code executes inside the pipeline

Once the impostor package is installed, any install-time code supported by that ecosystem can run inside the build environment using the permissions available to that job. In npm, for example, that includes install and lifecycle hooks. Attackers use that same foothold to harvest secrets, open reverse shells, or tamper with build artifacts — the same install-time execution that makes malicious post-install scripts so dangerous.

Why Traditional Tools Miss Dependency Confusion Attacks

Each standard control misses this class of attack for structural reasons, which is why build-time security for CI/CD pipelines has become a necessity.

SCA scanners

Software composition analysis identifies dependencies that are known to be vulnerable or malicious, usually by matching packages and versions against a database. The attacker’s package is brand-new, and by name it looks exactly like a component you legitimately use. For a new attack, there is no known issue for the scanner to match against yet. Traditional SCA may also not reliably enforce which registry a package resolved from, which is the entire point of the attack.

Dependency lockfiles

Lockfiles help only if the correct version was already pinned before the attack. They don’t protect a first install, a newly added dependency, or a floating range like latest, and a mis-resolved package can be written straight into a fresh lockfile as if it were legitimate.

Post-build artifact scanning

Artifact scanning runs after resolution and execution have already happened. By the time it looks, the malicious package has been fetched, and its install hook has already run. You’re inspecting the aftermath, not preventing the event.

How to Prevent Dependency Confusion at Build Time

Reserve internal names across public registries

Register your internal package names on npm, PyPI, and RubyGems — even as empty placeholders — so an attacker can never claim a name your build depends on.

Pin dependency versions and verify checksums

Commit your lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml, poetry.lock) with exact versions and integrity hashes. A pinned, checksum-verified dependency won’t be silently replaced by a surprise higher version.

Configure private registries with strict resolution rules

Use scoped packages (@yourcompany/package) and map that scope to your private registry in .npmrc, so anything under your scope never resolves against public npm. On the registry side, disable upstream proxying for your internal scope (for example, Verdaccio’s proxy: []) so it never forwards internal-scope requests to a public source. Avoid mixing a private and public index on the same resolution path, which is what makes –extra-index-url-style setups risky.

Inspect build-time network traffic

Route your builds through a controlled egress path, so you can see where each dependency is fetched from and block any attempt to pull a private package name from a public registry. This is the control that catches the attack in the act, regardless of how a package was named or versioned.

Stop Dependency Confusion Attacks with InvisiRisk

Every prevention step above works, but they’re all configuration you must get right in every project, every registry, and every pipeline, and keep right forever. Miss one unscoped legacy package or one misconfigured index, and the attack still resolves at build time, the layer traditional tools never watch.

InvisiRisk’s Build Application Firewall (BAF) closes that gap by sitting inside the build and enforcing policy on dependency retrieval itself: which packages may be pulled, from which registries, and under what conditions. A private package name resolving from a public registry, or a suspicious high-version substitution, can be flagged and blocked before the package is ever fetched, or before install-time behavior is allowed to proceed. A configurable stability buffer holds freshly published versions — exactly the newly registered impostors these attacks rely on — and, in a protected build, full egress control means every dependency pull is visible and governed.

Dependency confusion works because your build trusts a name and resolves it in the dark. The durable fix is to enforce, at build time, where that name is allowed to come from.

Frequently Asked Questions About Dependency Confusion

What is a dependency confusion attack?

A dependency confusion attack tricks your build into pulling a malicious public package in place of a legitimate private one. An attacker publishes a package to a public registry using an internal dependency’s name with a higher version number, and the resolver picks the public copy. It requires no stolen credentials or conventional software exploit, only the package name.

Do lockfiles stop dependency confusion?

Only partially. A lockfile protects a dependency already pinned to the correct version and source, but not a first install, a newly added dependency, or a floating range. A mis-resolved package can also be written straight into a fresh lockfile as if it were legitimate.

How do scoped packages reduce dependency confusion risk?

Scoped packages tie a name to your namespace (@yourcompany/package) mapped to your private registry, so it never resolves against public npm and an attacker cannot publish a colliding name. The catch: it only protects the names you actually scope, leaving unscoped legacy packages exposed.

How does build-time enforcement help stop dependency confusion?

Build-time enforcement applies policy to where each package may come from as it is retrieved, instead of relying on every project and registry being configured perfectly. A private package name resolving from a public registry can be flagged and blocked before it is fetched, which is what InvisiRisk’s Build Application Firewall (BAF) is built to do.

See how InvisiRisk secures the build pipeline: read Build-Time Security Explained for CI/CD Pipelines or request a demo.