Skip to content

Supply Chain Security & SRI

expert20 min read

Your Dependencies Are an Attack Surface

You write 5% of the code that runs in your application. The other 95% comes from npm. Every package you install is code you trust to run in your users' browsers and on your servers — and that trust is routinely exploited.

In 2018, the event-stream package was compromised when a new maintainer injected malicious code targeting the Copay Bitcoin wallet — affecting millions of downloads before detection. In October 2021, attackers hijacked ua-parser-js (a package with tens of millions of weekly downloads) and published versions that installed crypto miners and password stealers. In December 2024, North Korean state actors compromised @solana/web3.js through a stolen maintainer token.

This is not theoretical. Supply chain attacks are the most active and impactful attack vector in the JavaScript ecosystem.

Mental Model

Think of your dependency tree like a food supply chain. You buy ingredients (packages) from suppliers (npm authors). You trust that the flour is actually flour — not contaminated. But the flour supplier buys wheat from someone else, who buys seeds from someone else. A single poisoned link anywhere in the chain — a compromised npm account, a malicious transitive dependency, a tampered lockfile — and the contamination reaches your users. Supply chain security is food safety for code.

How Supply Chain Attacks Actually Work

1. Account Takeover (Most Common)

The attacker gains access to a maintainer's npm account — through phishing, credential stuffing, leaked tokens, or 2FA bypass. Then they publish a new patch version with malicious code.

debug@4.3.4  →  debug@4.3.5 (malicious)

Developers running npm update or whose lockfiles use ^4.3.4 (caret range) pull the malicious version automatically. The 2021 ua-parser-js compromise used exactly this technique — the maintainer's account was hijacked and malicious patch versions were published with crypto mining and credential theft payloads.

2. Typosquatting

The attacker publishes a package with a name similar to a popular one:

lodash    → lod-ash, lodashs, l0dash
express   → expresss, expres, express-js

A single typo in your npm install command installs the malicious package instead.

3. Dependency Confusion

If your company uses a private registry for internal packages (@company/auth-utils), an attacker publishes a public package with the same name but a higher version number. Some package manager configurations check the public registry first, pulling the attacker's package instead of yours.

4. Lockfile Poisoning

The attacker submits a PR to your open-source project that modifies package-lock.json or pnpm-lock.yaml to point to a malicious package URL or tampered integrity hash — while the package.json changes look innocent.

// In the lockfile, the tarball URL is changed:
"resolved": "https://evil-registry.com/debug/-/debug-4.3.4.tgz"
// Or the integrity hash is changed to match a tampered tarball

Most code reviewers don't read lockfile diffs carefully. The malicious URL gets merged.

Quiz
In the 2021 ua-parser-js npm supply chain attack, how did attackers distribute malicious code to millions of developers?

Subresource Integrity (SRI)

SRI lets you verify that a file loaded from a CDN or third-party server hasn't been tampered with. You include a cryptographic hash of the expected content, and the browser refuses to execute the file if the hash doesn't match.

<script
  src="https://cdn.example.com/lib@3.0.0/lib.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
  crossorigin="anonymous"
></script>

If the CDN is compromised and serves modified JavaScript, the browser calculates the hash of the received file, finds it doesn't match sha384-oqVu..., and blocks execution entirely.

Generating SRI Hashes

# Generate a hash from a local file
cat lib.min.js | openssl dgst -sha384 -binary | openssl base64 -A

# Or use the srihash.org web tool
# Or use the webpack/Vite SRI plugins for automated generation

SRI Best Practices

<!-- GOOD: pin the exact version and include integrity -->
<script
  src="https://cdn.example.com/react@19.0.0/umd/react.production.min.js"
  integrity="sha384-abc123..."
  crossorigin="anonymous"
></script>

<!-- BAD: no integrity check — CDN compromise = instant XSS -->
<script src="https://cdn.example.com/react@19.0.0/umd/react.production.min.js"></script>

<!-- WORSE: version range — content changes and old hash breaks -->
<script
  src="https://cdn.example.com/react@latest/umd/react.production.min.js"
  integrity="sha384-abc123..."
></script>

SRI Limitations

  • Only works for resources loaded via <script> and <link> tags with a crossorigin attribute
  • Doesn't protect against resources loaded by JavaScript at runtime (createElement('script'))
  • If the file legitimately changes (version update), the hash breaks — you must update the hash
  • Doesn't protect fetch() or XMLHttpRequest responses
Quiz
An application loads jQuery from a CDN with an SRI integrity attribute. The CDN is compromised and serves malicious JavaScript. What happens?

Lockfile Security

Your lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock) is a security artifact. It pins exact versions and records integrity hashes for every installed package. Treat it with the same scrutiny as source code.

What the Lockfile Contains

{
  "packages": {
    "node_modules/debug": {
      "version": "4.3.4",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
      "integrity": "sha512-...",
      "dependencies": { "ms": "2.1.2" }
    }
  }
}

The integrity field is an SRI hash of the tarball. When you run npm ci (which you should always use in CI), npm verifies the downloaded tarball against this hash. If someone tampers with the registry or performs a man-in-the-middle attack, the hash mismatch stops the install.

Lockfile Review Checklist

When reviewing PRs that modify lockfiles:

  1. Does the package.json change justify the lockfile changes? A one-line version bump should not change hundreds of lockfile entries
  2. Are all resolved URLs pointing to the expected registry? Watch for URLs to unknown registries
  3. Are there new packages you don't recognize? Check each new dependency on npm before merging
  4. Did integrity hashes change for packages that didn't update? This is suspicious
Always use npm ci in CI/CD

npm install can modify the lockfile. npm ci uses the lockfile exactly as committed — if there's a mismatch with package.json, it fails instead of silently fixing it. In CI/CD pipelines, always use npm ci (or pnpm install --frozen-lockfile) to ensure reproducible, tamper-evident builds.

Dependency Auditing Strategy

Automated Auditing

# npm's built-in audit
npm audit

# More detailed with severity filtering
npm audit --audit-level=high

# pnpm equivalent
pnpm audit

# Socket.dev for deeper analysis (detects install scripts, network access, etc.)
npx socket optimize

Manual Auditing Checklist for New Dependencies

Before adding any new dependency, check:

  1. Download count and trend — Is it actively used? Is usage growing or declining?
  2. Maintenance activity — When was the last commit? Last release? Are issues being addressed?
  3. Contributor count — Single-maintainer packages are higher risk for account takeover
  4. Install scripts — Does it run code during npm install? Check preinstall, postinstall scripts
  5. Permissions — Does it need network access? File system access? Check the source
  6. Transitive dependencies — How many sub-dependencies does it pull in? Each one is risk
  7. Size — Is a 2KB utility pulling in 50 dependencies? That's a red flag
# Check what install scripts a package runs
npm pack <package-name> --dry-run

# See the dependency tree
npm ls <package-name> --all

# Check package details
npm info <package-name>
Quiz
Why should CI/CD pipelines use npm ci instead of npm install?

Production Scenario: Defending a Large Frontend Application

Here's a practical security strategy for a team managing a production frontend with hundreds of dependencies:

1. Pin Dependencies to Exact Versions

{
  "dependencies": {
    "react": "19.0.0",
    "next": "15.2.0"
  }
}

No carets (^), no tildes (~). Exact versions prevent auto-updating to compromised patch versions. Use Dependabot or Renovate for controlled, reviewed updates.

2. Use a Private Registry Mirror

Mirror npm packages through a private registry (Artifactory, Verdaccio, GitHub Packages). This gives you:

  • A cache that survives npm outages
  • Audit logs of what's pulled
  • Ability to block known-malicious versions
  • Protection against dependency confusion (your private packages always win)

3. Disable Install Scripts for Untrusted Packages

# .npmrc
ignore-scripts=true

Then explicitly allow install scripts only for packages that need them:

{
  "scripts": {
    "postinstall": "node scripts/run-allowed-postinstalls.js"
  }
}

Most packages don't need install scripts. The ones that do (like esbuild downloading platform binaries) can be explicitly allowed.

4. Automate Security Scanning

Set up GitHub Actions or similar CI to:

  • Run npm audit on every PR
  • Use Socket.dev or Snyk for deeper analysis
  • Alert on new install scripts or network-accessing code
  • Block PRs that introduce high-severity vulnerabilities
What developers doWhat they should do
Using caret ranges for all dependencies and assuming npm handles security
Caret ranges (^1.2.3) auto-update to newer minor/patch versions. If an attacker publishes a compromised patch version, your next npm install pulls it automatically. Exact pinning plus controlled updates via PRs gives you a review step before any version change reaches production.
Pinning exact versions and using automated dependency update tools like Renovate
Only auditing direct dependencies and ignoring transitive ones
Transitive dependencies (deps of your deps) make up the vast majority of your node_modules. The event-stream attack in 2018 was through a transitive dependency. Use npm ls --all and npm audit to see and check the full tree.
Auditing the full dependency tree including transitive dependencies
Loading third-party scripts from CDNs without SRI integrity attributes
A CDN compromise or DNS hijack can serve malicious JavaScript to every user of your application. SRI ensures the browser rejects tampered content. Without it, you're trusting the CDN's security as much as your own — and CDNs are high-value targets.
Including integrity hashes on all externally loaded scripts and pinning exact versions
Challenge: Audit this package.json for supply chain risks

Try to solve it before peeking at the answer.

{
  "dependencies": {
    "react": "^19.0.0",
    "next": "^15.2.0",
    "lodash": "^4.17.21",
    "my-utils": "latest",
    "analytics-sdk": "https://analytics.example.com/sdk-3.2.1.tgz"
  },
  "scripts": {
    "postinstall": "node node_modules/analytics-sdk/setup.js"
  }
}
Key Rules
  1. 1Pin all dependencies to exact versions — caret ranges allow auto-updates to compromised patch releases
  2. 2Always use npm ci (or pnpm install --frozen-lockfile) in CI/CD — it verifies lockfile integrity and prevents tampering
  3. 3Add SRI integrity attributes to every externally loaded script — CDN compromises are real and common
  4. 4Review lockfile diffs in PRs with the same scrutiny as source code — lockfile poisoning is a proven attack vector
  5. 5Disable install scripts globally and allowlist only packages that genuinely need them
  6. 6Audit the full dependency tree, not just direct dependencies — supply chain attacks often target transitive packages