Skip to main content

Dependency Confusion and Supply Chain Attacks: Protecting Your npm Pipeline

June 2, 2026

The SolarWinds breach of 2020 demonstrated that the most effective way to compromise a large organization is not to attack it directly — it is to attack the software it trusts and installs. In 2026, the npm ecosystem is the most active attack surface for supply chain attacks targeting web developers, with new documented attacks occurring monthly.

Two attack types dominate: typosquatting (registering lodahs hoping someone typos lodash) and dependency confusion (a more sophisticated technique that exploits how npm resolves package names between public and private registries).


How Dependency Confusion Works

Most companies use a private npm registry (Artifactory, GitHub Packages, Verdaccio) for internal packages:

// company's package.json
{
  "dependencies": {
    "@mycompany/internal-ui": "2.1.0",  // Private package
    "react": "18.0.0"                    // Public package
  }
}

The attack:

  1. Attacker discovers the internal package name @mycompany/internal-ui (from job postings, leaked package.json files, or error messages).
  2. Attacker publishes a malicious package with the same name to the public npm registry.
  3. If the private registry is not configured correctly, npm resolves the public version (often a higher version number) instead of the private one.
  4. The malicious package runs in postinstall scripts with full system access.
DEPENDENCY CONFUSION:

npm install
    
    ├── Check private registry first?  @mycompany/internal-ui: v2.1.0
                                                                    
       OR (if misconfigured):                                      
                                                                   
    └── Check public registry first?  @mycompany/internal-ui: v9.9.9  ATTACKER'S PACKAGE

Alex Birsan's 2021 research demonstrated this attack worked against Apple, Microsoft, Netflix, Tesla, and Uber. Most were not using scoped private packages correctly.


Defense 1: Configure Private Registry Correctly

# .npmrc  force all @mycompany scoped packages to your private registry
@mycompany:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}

# CRITICAL: never allow public registry fallback for private packages
# In Artifactory: disable "Remote repositories" for your internal scope
# In GitHub Packages: scoped packages are automatically private-only

In Verdaccio (self-hosted):

# config.yaml
packages:
  '@mycompany/*':
    access: $authenticated
    publish: $authenticated
    # No 'proxy' entry = no public registry fallback for this scope
    
  '**':
    access: $all
    publish: $authenticated
    proxy: npmjs  # Public packages still fall through to npmjs

Defense 2: Lock All Dependency Versions

Never use ^ (caret) or ~ (tilde) version ranges for security-sensitive dependencies. Use exact versions:

{
  "dependencies": {
    "@mycompany/internal-ui": "2.1.0",   // Exact — safe
    "express": "4.18.2",                  // Exact — safe
    "lodash": "^4.17.0"                   // Range — allows 4.18.0 (potentially malicious)
  }
}

And always use --frozen-lockfile in CI:

- name: Install dependencies
  run: pnpm install --frozen-lockfile
  # Fails if package-lock.json doesn't match package.json
  # Prevents any unexpected version resolution

Defense 3: Automated Package Scanning

# .github/workflows/supply-chain-security.yml
name: Supply Chain Security

on:
  push:
    branches: [main]
  pull_request:

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Standard CVE database check
      - name: npm audit
        run: npm audit --audit-level=high

      # Socket.dev: detects behavioral anomalies (install scripts, obfuscation)
      - name: Socket Security Scan
        uses: SocketDev/socket-security-action@v1
        with:
          api-key: ${{ secrets.SOCKET_API_KEY }}
          allow-new-deps: false  # Require explicit approval for new packages

      # Snyk: dependency vulnerability scanning
      - name: Snyk Security Scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high

Socket.dev specifically addresses supply chain attacks by analyzing behavioral signals — not just CVE databases:

  • Does the package run install scripts? (Most legitimate packages don't.)
  • Does it obfuscate its code?
  • Does it make network requests during installation?
  • Was it recently published by a new maintainer (common in account hijacking)?

Defense 4: Subresource Integrity for CDN Scripts

If you load scripts from CDNs, use Subresource Integrity (SRI) hashes:

<!-- Without SRI: trusting the CDN completely -->
<script src="https://cdn.example.com/lodash.min.js"></script>

<!-- With SRI: browser verifies the file hash before executing -->
<script
  src="https://cdn.example.com/lodash.min.js"
  integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Iio"
  crossorigin="anonymous">
</script>

Generate SRI hashes:

curl -s https://cdn.example.com/lodash.min.js | openssl dgst -sha384 -binary | openssl base64 -A

Defense 5: Allowlist Approved Packages

For high-security environments, maintain an explicit allowlist of approved packages and fail CI on any unlisted dependency:

// scripts/check-approved-packages.js
import { readFileSync } from 'fs';

const APPROVED_PACKAGES = new Set([
  'react', 'react-dom', 'next', 'typescript',
  // ... your approved list
]);

const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
const allDeps = {
  ...packageJson.dependencies,
  ...packageJson.devDependencies,
};

const unapproved = Object.keys(allDeps)
  .filter(dep => !dep.startsWith('@mycompany/'))
  .filter(dep => !APPROVED_PACKAGES.has(dep));

if (unapproved.length > 0) {
  console.error('Unapproved packages detected:', unapproved);
  console.error('Add these to the approved list after security review.');
  process.exit(1);
}

Red Flags to Watch For

Treat these as immediate security concerns in any npm package:

  • postinstall scripts that execute arbitrary commands.
  • Network requests during installation (install.js that calls external URLs).
  • Obfuscated code (minified in source files, base64-encoded strings).
  • Unusually high version numbers for packages you don't recognize (dependency confusion indicator).
  • New maintainer on an old, popular package (account hijacking).
  • Package name closely resembles a popular package (colurs vs colors, loadsh vs lodash).

Conclusion

Supply chain attacks through npm are not theoretical — they have compromised some of the world's largest organizations. The defense is layered: configure your private registry to prevent public fallback for internal package names, use exact version locking in CI, scan every PR for behavioral anomalies with Socket.dev, and use --frozen-lockfile to prevent unexpected resolution changes. The 30 minutes it takes to implement this stack is a small investment against the cost of a supply chain breach.

Recommended Posts