In February 2021, a security researcher named Alex Birsan executed arbitrary code inside the build systems of Apple, Microsoft, PayPal, Shopify, Netflix, and dozens of other companies โ without breaking into a single one of them. He did it by uploading packages to public registries. The companies had installed them themselves.
Total bug bounty payout: $130,000. The technique is still working in 2026.
What the attack is
Most large engineering organizations maintain a private package registry โ internal npm packages, private Python libraries, internal Go modules. These are not on npm or PyPI; they live on private infrastructure.
Dependency confusion exploits a specific behavior in how package managers resolve names: when they look for a package, they check both the private registry and the public one. In many organizations, the resolution order is not explicitly configured โ which means it defaults to preferring the public registry.
The attack in its simplest form:
- An attacker discovers the name of an internal package โ
@mycompany/data-clientorinternal-utils. - They register a package with that exact name on the public registry.
- They assign it a higher version number โ
9.9.9โ because package managers prefer the highest available version. - The next time a developer runs
npm install, the package manager installs the attacker's code instead of the internal one.
The developer sees no error. The build completes normally. The attacker's code runs in the build environment.
How Birsan found the attack surface
Birsan did not guess package names. He found them through sources organizations don't think of as security-sensitive.
Job postings mention internal tools by name. Public error logs and stack traces contain import paths for internal packages. Public GitHub repositories sometimes reference internal packages in package.json or requirements.txt. He then checked which internal names were unregistered on public registries โ and registered them himself before reporting the vulnerabilities.
How the technique has evolved
Early dependency confusion attacks required manual reconnaissance. Current tooling automates it. Attackers run scripts that monitor public GitHub repositories, scrape package registry metadata, and cross-reference job listings continuously. Security researchers tracking dependency confusion in 2025 found the average time between a company's internal package name appearing publicly and a corresponding malicious public package being registered is now measured in days, not months.
More critically: dependency confusion has moved from a bug bounty technique to a standard initial access tool in targeted attacks. Threat actors now upload fully functional malware, not benign beacons.
How to tell if you're vulnerable
Check internal package names against public registries. For every internal package name, verify it is not registered on the public registry under a name you don't own.
# npm
npm info @yourcompany/package-name 2>&1 | head -5
# PyPI
pip index versions your-internal-package 2>&1
Audit your registry configuration. For npm, check .npmrc:
# Correct โ your scope is pinned to your private registry
@yourcompany:registry=https://your-private-registry.internal
If your company scope has no explicit registry mapping, internal scoped packages may resolve publicly.
For pip, understand the difference between --index-url (replaces the public index) and --extra-index-url (adds to it โ and pip takes the highest version across all sources). Using --extra-index-url with a public fallback is the vulnerable configuration.
Mitigations
Claim your package names on public registries. Register stub packages on npm and PyPI for every internal name you use. It is not elegant, but it is the most reliable defense against an attacker claiming the name first.
Explicit registry pinning per scope. In .npmrc, map every internal scope to your private registry explicitly:
@yourcompany:registry=https://registry.your-internal.com
always-auth=true
For pip, use --index-url (not --extra-index-url) for packages that must only come from an internal source. The difference is critical: --index-url replaces the default index; --extra-index-url adds to it and pip takes the highest version across all sources.
Use lockfiles and enforce them in CI. npm ci installs exactly what the lockfile specifies and fails if it doesn't match. npm install can silently resolve to a different source if registry configuration changes. For pip, --require-hashes enforces hash verification โ every package must match a known SHA256, blocking any package not explicitly approved regardless of source.
Block public resolution for known-internal package names. Artifactory and Nexus both support explicit exclusion lists: package names that must never be resolved from public sources. Configure them for every internal package name.
Detection signals
Unexpected resolution sources in install logs โ internal package names resolving to registry.npmjs.org or pypi.org instead of your internal registry โ are the primary indicator. Post-install scripts running for packages that have never needed them is another. Hash mismatches on lockfile verification are frequently dismissed as "lockfile out of date"; investigate before dismissing.
The fix is not technically complex. Claim your package names. Pin your package managers to internal sources by scope. Use lockfiles and enforce them in CI. The gap between knowing what to do and having done it is where the attacks happen.
Lumstep's open-source trust scoring checks package names against known public registries and flags packages that appear under internal-looking names without a public code history โ one signal for catching dependency confusion before the build completes.
Or let Lumstep handle it.
Connect a repo and Lumstep scans it automatically - secrets, dependencies, SBOM, and code quality - and opens the fix PR.