Supply Chain Security & SRI
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.
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.
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 acrossoriginattribute - 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()orXMLHttpRequestresponses
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:
- Does the
package.jsonchange justify the lockfile changes? A one-line version bump should not change hundreds of lockfile entries - Are all
resolvedURLs pointing to the expected registry? Watch for URLs to unknown registries - Are there new packages you don't recognize? Check each new dependency on npm before merging
- Did
integrityhashes change for packages that didn't update? This is suspicious
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:
- Download count and trend — Is it actively used? Is usage growing or declining?
- Maintenance activity — When was the last commit? Last release? Are issues being addressed?
- Contributor count — Single-maintainer packages are higher risk for account takeover
- Install scripts — Does it run code during
npm install? Checkpreinstall,postinstallscripts - Permissions — Does it need network access? File system access? Check the source
- Transitive dependencies — How many sub-dependencies does it pull in? Each one is risk
- 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>
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 auditon 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 do | What 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 |
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"
}
}- 1Pin all dependencies to exact versions — caret ranges allow auto-updates to compromised patch releases
- 2Always use npm ci (or pnpm install --frozen-lockfile) in CI/CD — it verifies lockfile integrity and prevents tampering
- 3Add SRI integrity attributes to every externally loaded script — CDN compromises are real and common
- 4Review lockfile diffs in PRs with the same scrutiny as source code — lockfile poisoning is a proven attack vector
- 5Disable install scripts globally and allowlist only packages that genuinely need them
- 6Audit the full dependency tree, not just direct dependencies — supply chain attacks often target transitive packages