Software Supply Chain Security

Dependency Confusion in Private Registries: The Attack That Keeps Working

Dependency confusion exploits the gap between public and private package registries. Despite widespread awareness, organizations keep falling for it.

Michael
Frontend Security Engineer
5 min read

In February 2021, Alex Birsan published research demonstrating that he could execute code inside the networks of Apple, Microsoft, PayPal, Netflix, Tesla, and dozens of other companies by publishing packages with specific names to public registries. The technique -- dependency confusion -- exploited a gap in how package managers resolve dependencies when both public and private registries are configured.

Three years later, organizations are still falling for this attack. The technique is simple, the defenses are well-documented, and yet it keeps working.

How Dependency Confusion Works

Most organizations have private packages that are only used internally. These packages have names like company-auth-utils or internal-logging-lib and are hosted on a private registry (Artifactory, Nexus, GitHub Packages, GitLab Package Registry, Azure Artifacts).

The attack exploits the fact that package managers often check public registries alongside (or before) private ones. If an attacker publishes a package with the same name on the public registry (npm, PyPI, RubyGems) with a higher version number, the package manager may prefer the public version.

The attacker's package includes an install script that phones home with information about the machine it was installed on -- or worse, establishes a reverse shell, exfiltrates credentials, or installs a backdoor.

Why It Still Works

Default registry configuration. npm, pip, and other package managers default to public registries. Adding a private registry often adds it alongside the public one rather than replacing it. The exact resolution behavior depends on the tool and configuration, and many teams get it wrong.

Scope/namespace inconsistency. npm has scoped packages (@company/package-name) that can be mapped to a private registry. But not all private packages use scopes. PyPI does not have a scope concept at all. Maven has group IDs but the mapping to repositories is indirect.

CI/CD environment drift. Developer workstations might be correctly configured, but CI/CD runners might not. A runner that was configured before the private registry was set up, or one that was cloned from an outdated template, might still use the default public resolution.

Transitive dependency resolution. Even if your direct dependencies are correctly resolved, their dependencies might not be. A private package that depends on another private package by name (without registry specification) can trigger public resolution for the transitive dependency.

Package manager lock file limitations. Lock files pin versions but do not always pin registry sources. A lock file that says "package-name@1.0.0" does not specify which registry that version should come from.

Ecosystem-Specific Vulnerabilities

npm: The .npmrc file can map scoped packages to specific registries, but unscoped packages always resolve from the default registry. If your private packages are unscoped, they are vulnerable.

pip: pip's --index-url replaces the default index, but --extra-index-url adds an additional source. If both are configured, pip tries the extra index and can fall back to PyPI. The --index-url approach is safer but breaks if any dependency is only on PyPI.

Maven: Maven resolves dependencies from repositories listed in pom.xml and settings.xml. If both Maven Central and a private repository are configured, Maven checks both. The resolution order depends on configuration and can be surprising.

Go: Go's module proxy (proxy.golang.org) is the default for all public modules. Private modules must be excluded via GONOSUMCHECK and GOPRIVATE. If these are not configured, Go will attempt to resolve private module names through the public proxy.

NuGet: NuGet resolves packages from all configured sources. The packageSourceMapping feature (introduced in NuGet 6.0) allows mapping specific package prefixes to specific sources, which is the correct defense.

Prevention

Use scoped/namespaced packages. On npm, always use organization scopes (@company/). On Maven, use a group ID you control. On NuGet, use package prefixes with source mapping.

Claim your names on public registries. If your private packages have names that could be claimed on public registries, claim them yourself and publish placeholder packages. This is defensive namespace reservation.

Configure registry priority correctly. Use --index-url (not --extra-index-url) for pip. Use scoped registry mapping for npm. Use packageSourceMapping for NuGet. Use GOPROXY with direct fallback for Go.

Monitor public registries. Set up alerts for when packages with names matching your private packages appear on public registries. This early warning lets you respond before the attack succeeds.

Lock file integrity. Ensure your lock files capture the source registry, not just the package name and version. Review lock file changes that add packages from unexpected sources.

Network segmentation. CI/CD runners that build private code should not have unfettered access to public registries. Route all package resolution through a managed proxy that enforces resolution policies.

How Safeguard.sh Helps

Safeguard.sh monitors public package registries for packages that match your private package names, providing early warning of dependency confusion attempts. Our platform also analyzes your project configurations to identify misconfigurations that leave you vulnerable to dependency confusion. Combined with SBOM generation that captures the source registry for each dependency, Safeguard.sh gives you the visibility to prevent and detect dependency confusion attacks before they compromise your build pipeline.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.