What is Software Supply Chain Security?
Software supply chain security addresses the risks associated with the external code, tools, and services that your application depends on. The average JavaScript project has hundreds of transitive dependencies, each of which is a potential attack vector. A single compromised dependency can affect millions of applications downstream — as demonstrated by incidents like the event-stream, ua-parser-js, and colors.js attacks.
Supply chain attacks are particularly dangerous because they exploit trust. When you install a package from npm, you are trusting that the package author, the registry, the build system, and every transitive dependency has not been compromised. A supply chain attack can inject malicious code into your application without you changing a single line of your own code.
Security Warning: Supply Chain Attack Vectors
- Typosquatting: Publishing malicious packages with names similar to popular packages (e.g., "lodahs" instead of "lodash").
- Compromised maintainer accounts: Attackers gain access to a maintainer's npm account and publish a malicious update.
- Malicious postinstall scripts: Packages that run arbitrary code during npm install via lifecycle scripts.
- Dependency confusion: Publishing public packages with the same name as private/internal packages, causing them to be installed instead.
- Protestware: Maintainers intentionally sabotaging their own popular packages for political or personal reasons.
Lockfile Security
Lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) pin exact dependency versions and integrity hashes. They ensure that every install produces identical results and prevent unexpected updates from introducing malicious code. Always commit your lockfile and use install commands that respect it.
// .npmrc - Secure npm configuration
// Put this in your project root
// Always use the lockfile for installs (fail if it needs updating)
// Use 'npm ci' in CI/CD instead of 'npm install'
// Disable postinstall scripts from dependencies (major security improvement)
// ignore-scripts=true
// Then explicitly allow trusted packages:
// @prisma/client:install=true
// Use a specific registry (prevent dependency confusion)
// registry=https://registry.npmjs.org/
// @mycompany:registry=https://npm.mycompany.com/
// Enable package provenance verification
// provenance=true
// Automated dependency security checks
// package.json scripts for security
{
"scripts": {
"security:audit": "npm audit --audit-level=high",
"security:check": "npx better-npm-audit audit --level high",
"security:licenses": "npx license-checker --failOn 'GPL-3.0;AGPL-3.0'",
"security:outdated": "npm outdated",
"preinstall": "npx lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https"
}
}
// CI/CD security checks (GitHub Actions)
// .github/workflows/security.yml
/*
name: Security Checks
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci # Uses lockfile, fails if it's out of date
- run: npm audit --audit-level=high
- run: npx lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https
*/
Dependency Pinning and Review
Pin your direct dependencies to exact versions (no caret ^ or tilde ~ prefixes) to prevent unexpected updates. Use tools like Renovate or Dependabot for controlled, reviewable updates. Before adding a new dependency, evaluate it for security: check the maintainer's reputation, download count, last update date, and known vulnerabilities.
// Script to evaluate dependency health before adding
import { execSync } from "child_process";
interface DependencyHealth {
name: string;
version: string;
weeklyDownloads: number;
lastPublish: string;
maintainers: number;
dependencies: number;
hasTypes: boolean;
license: string;
score: number;
}
async function evaluateDependency(packageName: string): Promise<DependencyHealth> {
// Check npm registry data
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
const data = await response.json();
const latest = data["dist-tags"].latest;
const latestData = data.versions[latest];
const lastPublish = data.time[latest];
const daysSincePublish = Math.floor(
(Date.now() - new Date(lastPublish).getTime()) / (1000 * 60 * 60 * 24)
);
// Calculate health score
let score = 100;
if (daysSincePublish > 365) score -= 20; // Unmaintained
if (data.maintainers.length < 2) score -= 10; // Bus factor
if (Object.keys(latestData.dependencies || {}).length > 20) score -= 15; // Too many deps
if (!latestData.types && !latestData.typings) score -= 5; // No TypeScript types
return {
name: packageName,
version: latest,
weeklyDownloads: 0, // Would need npms.io API
lastPublish,
maintainers: data.maintainers.length,
dependencies: Object.keys(latestData.dependencies || {}).length,
hasTypes: !!(latestData.types || latestData.typings),
license: latestData.license || "UNKNOWN",
score,
};
}
// Usage in a pre-add hook
async function shouldAddDependency(name: string): Promise<boolean> {
const health = await evaluateDependency(name);
if (health.score < 50) {
console.warn(`WARNING: ${name} has a low health score (${health.score}/100)`);
console.warn("Consider finding an alternative or vendoring the code.");
return false;
}
if (health.license === "UNKNOWN" || health.license.includes("GPL")) {
console.warn(`WARNING: ${name} has license: ${health.license}`);
return false;
}
return true;
}
Supply Chain Security Best Practices
- Use npm ci in CI/CD: Never npm install in CI — use npm ci to install from the lockfile exactly.
- Pin dependencies: Use exact versions in package.json and commit lockfiles.
- Audit regularly: Run npm audit on every build and address high/critical findings immediately.
- Minimize dependencies: Before adding a package, ask if you can write the functionality yourself in a reasonable amount of code.
- Use Subresource Integrity: For any scripts loaded from CDNs, include SRI hashes.
- Scope internal packages: Use npm scopes (@company/package) and a private registry for internal packages to prevent dependency confusion.
- Review dependency updates: Never blindly merge dependency update PRs. Review changelogs and diffs.