Application Security

Dependency Confusion Exploits Which Registry Answers First

Key takeaway: The vulnerability is resolution order. When a package manager consults both a private and a public source, whichever provides a higher version wins — and anyone can publish a high version publicly.

The Mechanism

An organisation maintains an internal package called acme-common, published to a private registry, currently at version 2.1.0.

The build tool is configured with both the private registry and the public one. An attacker discovers the name — from a leaked manifest, a public repository, a job posting or an error message — and publishes acme-common version 99.0.0 to the public registry.

On the next build, the resolver sees two sources offering the name, selects the highest version, and installs the attacker’s package. Its install script executes with whatever access the build has, which typically includes cloud credentials and source code.

No system was compromised to achieve this. The resolver behaved as configured.

Why It Keeps Happening

Package managers were designed for public ecosystems where one registry served one namespace. Adding a private registry alongside the public one creates an ambiguity the original design never contemplated.

Internal package names also leak far more readily than teams expect. They appear in committed lockfiles, in stack traces posted publicly, in Docker layer metadata, in front-end bundles, and in documentation. The name is not a secret and cannot be treated as one.

Defences That Actually Close It

Defence Effectiveness
Namespace reservation (scopes) High
Registry pinning per package or scope High
Single upstream-proxying registry High
Defensive public placeholder publication Moderate
Version pinning with lockfile Partial — protects existing installs

Scoped names are the cleanest answer where the ecosystem supports them. @acme/common cannot be published by anyone who does not control the @acme organisation, which removes the ambiguity structurally.

Configuring resolution so that internal names come only from the internal registry is equally effective and works where scopes do not. The key property is that a name never has two possible sources.

A single registry that proxies the public one is the strongest architectural answer. The build tool has exactly one upstream, so resolution order cannot be manipulated — and it provides caching and auditability as side benefits.

Publishing placeholder packages under your internal names publicly is a defensive measure some organisations take. It prevents the specific attack for known names and does nothing for names created afterwards, so it complements rather than replaces configuration.

Verifying Your Exposure

The test is direct: pick an internal package name, query the public registry, and confirm it does not exist there. Then inspect your resolver configuration and confirm that internal names have exactly one possible source.

Build pipelines deserve particular attention because they combine high privilege with automated dependency resolution. A build runner with cloud credentials and an ambiguous resolver is the highest-value target in this class.

The Bottom Line

Use scoped package names or pin internal names to the internal registry so no name has two sources. Prefer a single proxying registry as the architectural fix, and audit your build pipelines first since they hold the credentials worth stealing.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button