Dependency Confusion Attacks: How They Work and How to Prevent Them
Learn how dependency confusion attacks exploit private package namespaces, and how to defend your supply chain with scoped packages and internal registries.
Dependency Confusion Attacks: How They Work and How to Prevent Them
In February 2021, security researcher Alex Birsan demonstrated something alarming: by publishing public packages with the same names as internal packages used by Apple, Microsoft, PayPal, and 33 other companies, he could trigger their build systems to download and execute his code. He earned over $130,000 in bug bounties for this research. The attack vector — now called dependency confusion — remains a live threat in nearly every organization that uses private packages alongside public registries.
What Is Dependency Confusion?
Package managers like npm, pip, and RubyGems resolve package names by searching one or more registries. When a build tool is configured with both a private internal registry and a public one, a race condition emerges: if your private package @company/auth-utils is searched against the public npm registry and a malicious version exists there, the package manager may download the public (attacker-controlled) version instead of the internal one.
The mechanics differ slightly between ecosystems:
- npm: Without scoped packages or explicit registry pinning, npm checks the public registry for any package it cannot find locally.
- pip: When
--extra-index-urlis used instead of--index-url, pip will pull from whichever registry returns the highest version number — public or private. - NuGet: Similar to pip, NuGet can be configured with multiple sources and may resolve to the highest version available across all of them.
The Attack in Practice
An attacker who discovers your internal package names (often leaked through package-lock.json files in public repos, job postings, or error messages) publishes a package with the same name to the public registry. They set the version number extremely high — say 9999.0.0 — to ensure it wins any version comparison. The package's install script runs arbitrary code on your build machine.
// Leaked in a public package-lock.json
{
"dependencies": {
"acme-internal-utils": "^1.2.0",
"acme-auth-core": "^3.0.1"
}
}
An attacker sees acme-internal-utils and acme-auth-core, publishes those names to npm with version 9999.0.0 and a malicious preinstall script.
Defense Strategy 1: Use npm Scopes
The most effective defense for npm is scoping all internal packages under your organization's namespace. Scoped packages (@acme/auth-core) can be pinned to a specific registry, preventing them from ever resolving against the public npm registry.
# .npmrc — pin the @acme scope to your private registry
@acme:registry=https://registry.your-company.com/npm/
With this configuration, npm will only look at your internal registry for any package beginning with @acme/. The public registry is never consulted for those names. Register your scope on npmjs.com (it's free) to prevent anyone else from publishing under it.
Defense Strategy 2: Fix pip's Index Resolution
The --extra-index-url flag is the root cause of most pip confusion attacks. It tells pip to search additional registries but lets the highest version win across all of them. Replace it with --index-url to use your private registry as the sole source, or use --extra-index-url with explicit version pinning and hash verification.
# Dangerous — attacker can win with a higher version
pip install --extra-index-url https://pypi.org/simple/ acme-utils
# Safer — private registry is authoritative
pip install --index-url https://pypi.your-company.com/simple/ acme-utils
# Best — lock to a specific hash
pip install acme-utils==1.2.3 \
--hash=sha256:abc123...
For projects using requirements.txt, use pip-compile with --generate-hashes to pin every dependency to a verified hash.
Defense Strategy 3: Set Up an Internal Registry with Upstream Proxying
Tools like Artifactory, Nexus, and AWS CodeArtifact act as a single source of truth for all packages — both internal and proxied-from-public. When a developer requests lodash, the internal registry fetches and caches it from npm. When they request @acme/auth-core, it serves the internal version.
# npm config pointing all traffic to Artifactory
npm config set registry https://artifactory.your-company.com/api/npm/npm-virtual/
This approach eliminates the multi-registry race entirely. Your build systems only talk to one registry, and that registry controls what versions are available.
Defense Strategy 4: Claim Your Package Names Publicly
For packages that cannot be scoped, publish a placeholder on the public registry under the same name. The placeholder should contain no code, just a clear README explaining this is an internal package name reservation, and a version number higher than any internal version you use.
{
"name": "acme-internal-utils",
"version": "1.0.0",
"description": "Reserved. This package is for internal use at Acme Corp.",
"scripts": {}
}
This is a low-effort mitigation that closes the namespace squatting window.
Defense Strategy 5: Run npm audit in CI
npm audit catches known vulnerabilities in public packages but does not directly detect confusion attacks. However, integrating it into your CI pipeline alongside integrity checks does give you a last line of defense.
# GitHub Actions example
- name: Install dependencies
run: npm ci
- name: Audit dependencies
run: npm audit --audit-level=high
- name: Verify package integrity
run: npm audit signatures
The npm audit signatures command (available since npm 8.x) verifies that every installed package was signed by the registry that produced it. A confused package from the wrong registry will fail this check.
Detecting Confusion Attempts
Monitor your internal registry logs for requests for packages that do not exist internally. Any request for an unknown package name that matches your internal naming conventions is a red flag.
Also monitor public registries for newly published packages that match your internal names:
# Check if a package name exists on npm
npm view acme-internal-utils --json 2>/dev/null || echo "Not on public registry"
Set up alerts using a tool like Socket.dev or Phylum that monitors new package publications and flags names similar to your internal packages.
Auditing Your Exposure
Run a quick audit of your package manifests to identify unscoped internal packages:
# Find all packages in node_modules not under a scope
ls node_modules | grep -v '^@' | sort > installed_unscoped.txt
# Compare against your known internal packages list
comm -23 installed_unscoped.txt known_public_packages.txt
Any package in that output that is not clearly a known public package deserves scrutiny.
Key Takeaways
- Dependency confusion attacks exploit multi-registry resolution order, not code vulnerabilities.
- Scope all internal npm packages under an organization namespace and pin that scope to your private registry in
.npmrc. - Replace
pip --extra-index-urlwith--index-urlor hash-pinned requirements. - Use a single internal registry with upstream proxying as your sole package source.
- Claim public namespaces for any internal package names you cannot scope.
- Add
npm audit signaturesto CI to catch packages from unexpected registries.