blog.rust.gay

Dependency confusion attacks

Dependency confusion is a supply-chain attack where a package manager, build system, or developer installs a public package that has the same name as an intended internal dependency. The attacker does not need to compromise the private repository first. If the target system can be induced to resolve the wrong package, installing that package can run attacker-controlled code during install, build, or runtime.

The attack is sometimes described as namespace confusion or dependency substitution. The core problem is not only that a name exists in two places. The risk comes from how tools decide which source wins when names collide, and from how often internal package names leak into public code, JavaScript bundles, forum posts, bug reports, or repository metadata. Once a name is known, an attacker can try to publish a package under that name to a registry that a target resolver might consult.

The 2021 disclosure that named the pattern

In February 2021, security researcher Alex Birsan published “Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies.” Birsan described finding internal package names in public artifacts, then uploading packages with those names to npm, PyPI, and RubyGems. His test packages collected limited identifying information and sent it back through DNS queries so he could tell where installations occurred.

Birsan reported that the technique affected more than 35 organizations across the three ecosystems he tested. He wrote that almost 75% of logged callbacks came from npm packages, which he attributed partly to JavaScript dependency names being easier to find in public artifacts such as bundled files and package.json data. He also named several affected companies in his report, including Shopify, Apple, PayPal, Netflix, Yelp, and Uber, and described bug bounty awards from some of them. Those details are Birsan’s account of authorized testing; readers evaluating impact should treat the original report as the primary source for those claims.

The disclosure was important because it showed that internal naming was not a private implementation detail. Names can leak, and resolvers can make surprising choices when a public package appears with the same name. The lesson is not limited to the ecosystems Birsan tested: any workflow that combines private dependencies, public registries, and ambiguous resolution rules deserves the same scrutiny.

Why package resolution rules matter

Dependency confusion is often enabled by a tool choosing the “best” package across multiple sources. Birsan described a Python example involving pip install with --extra-index-url: pip can check the specified index and the public package index, then install the version found from the source with the higher version number. In that scenario, publishing a malicious package with a very high version to the public index could cause it to be selected over the intended internal package.

Current pip documentation warns about this behavior. Its install documentation states that using --extra-index-url to search for packages not in the main repository is unsafe and calls it dependency confusion: an attacker can publish a package with the same name to a public index, which may then be chosen instead of a private package. That warning is the right default mental model for any multi-index setup. If a resolver considers multiple registries and uses version, availability, or order in a surprising way, name collisions become an install-time security decision.

npm has a different control surface. npm scopes prefix package names with @scope/, and a scope can be associated with a registry. Scoped packages are installed under a scope folder such as node_modules/@myorg/mypackage, and organizations can control names within their scope. Scopes do not eliminate supply-chain risk by themselves, but they reduce the chance that an internal package name is confused with an unrelated unscoped public name and make registry binding easier to express.

Cargo and Rust: different defaults, same need for clarity

Cargo’s model is not identical to pip’s multi-index behavior, but the same principle applies: package origin must be explicit and predictable. Cargo installs crates and fetches dependencies from registries, with crates.io as the default registry. To use an alternate registry, Cargo’s documentation describes adding the registry name and index URL to .cargo/config.toml under the [registries] table. A dependency can then select that registry with a registry key in Cargo.toml.

This design gives Rust projects a way to bind a dependency to a named source rather than relying on ambiguous cross-registry resolution. It also means configuration matters. Teams using private registries should review .cargo/config.toml, environment variables such as CARGO_REGISTRIES_*_INDEX, CI images, and developer bootstrap scripts so the intended registry is used consistently.

Cargo’s registry documentation also notes that crates.io does not accept packages that depend on crates from other registries. That constraint is useful for public publishing hygiene, but it does not remove the need to protect private builds. If an internal project depends on private crates or mirrors, the security question is whether every build environment resolves those names from the intended source.

Rust projects should also be careful with source replacement, mirrors, and vendoring workflows. These features can improve reliability and control, but a misconfigured replacement source can change where code comes from. The same is true for any wrapper around cargo in CI: if a script rewrites configuration, selects indexes dynamically, or installs dependencies before credentials and source bindings are in place, it can create an unexpected trust boundary.

Defenses that reduce the attack surface

The most reliable defenses make package origin explicit and predictable.

  • Use namespaces or scopes where the ecosystem supports them. npm scopes are a clear example. In ecosystems with flatter public namespaces, choose internal naming carefully and assume names may leak. A distinctive name is not a security control by itself, but it can reduce accidental collisions and make monitoring easier.
  • Bind package sources to the intended registry. In npm, associate a scope with the correct registry. In Python environments, avoid unsafe multi-index patterns when installing private packages and prefer configuration that makes the source of truth explicit. In Cargo, use named registries and the dependency registry key where appropriate, and keep registry configuration consistent across developer machines and CI.
  • Pin and review dependency changes. Lockfiles, checksums where supported, and reviewed dependency updates make unexpected substitutions more visible. A lockfile is not a substitute for registry controls, but it helps detect when resolution changes. For Cargo projects, commit and review Cargo.lock changes in applications, and be deliberate about updates in CI.
  • Treat leaked internal names as sensitive metadata. Birsan noted that internal names can appear in package.json files embedded in public JavaScript, leaked paths, and other artifacts. Internal naming conventions should assume names may escape into public repositories, client bundles, logs, screenshots, or issue trackers.
  • Reserve or monitor names where practical. If an ecosystem allows it, registering placeholder packages or monitoring for newly published packages that match internal naming patterns can provide early warning. This is a compensating control, not a complete fix. It is most useful when combined with deterministic resolution.
  • Audit install-time and build-time behavior. Dependency confusion often succeeds during npm install, pip install, or similar build steps. Cargo builds can also execute code through build scripts and procedural macros, so fetching the wrong crate is not just a metadata problem. Least privilege for CI runners, restricted network egress, isolated build environments, and fast incident response limit the blast radius if a malicious package is fetched.

What dependency confusion is not

Dependency confusion is related to, but different from, typosquatting. Typosquatting depends on a human or script choosing a lookalike name. Dependency confusion depends on automation choosing the wrong package for an existing name. It is also different from compromising a maintainer account or publishing a malicious update to an already trusted package. In those cases the name is correct and the trust relationship is abused later.

Because of that distinction, scanning only for known vulnerable versions is not enough. A dependency confusion package may be new, intentionally short-lived, and never associated with a CVE. The main preventive control is deterministic resolution: the right package name must come from the right source every time. Detection still matters — audit logs, dependency diffs, registry monitoring, and unusual install behavior can catch mistakes — but prevention is primarily a configuration and workflow problem.

Conclusion

Dependency confusion turns naming and resolution into security boundaries. Birsan’s 2021 research made the pattern concrete across npm, PyPI, and RubyGems, and current tooling documentation still treats mixed-source resolution as a real risk. Rust teams are not exempt just because Cargo’s defaults differ from pip’s: private registries, source replacement, CI configuration, and leaked names can still create ambiguity. The practical response is to assume private names can escape, choose names and namespaces deliberately, configure registries explicitly, review dependency resolution in the same environments that build and deploy code, and treat unexpected package-source changes as security events.

Sources


← back to posts