06 - Supply Chain Security: npm audit, SBOM and Dependency Management
September 2025 marked a turning point in software security history: eighteen of the most downloaded npm packages in the world, including chalk, debug, and ansi-styles, were compromised through a targeted phishing campaign against their maintainers. Within hours, over 2.6 billion weekly downloads were exposed to obfuscated JavaScript designed to intercept cryptocurrency transactions. This was not an attack on your application, but on the ecosystem that supports it.
This scenario illustrates the fundamental nature of supply chain attacks: they target not
the code you write, but the code you trust. Every dependency you install with
npm install represents code written by others, maintained by others, and
potentially compromised by others. In a typical Node.js project, direct dependencies are
usually 20-50 packages, but the full transitive dependency graph can easily reach
500-1000 libraries. Are you really auditing all of that code?
According to OWASP Top 10:2025, the A03: Software and Data Integrity Failures category explicitly includes supply chain failures as a critical attack vector. This is one of the two new categories introduced in the 2025 edition, an explicit recognition that code security can no longer be separated from dependency security. This article gives you the concrete tools to protect yourself.
What You Will Learn
- Anatomy of a supply chain attack: typosquatting, dependency confusion, lockfile poisoning
- npm audit, yarn audit, pnpm audit: advanced usage and automation
- Lockfile integrity and hash verification with npm ci
- SBOM: generation with CycloneDX and SPDX, NTIA standards
- Snyk and Dependabot: continuous vulnerability monitoring
- GitHub Actions hardening: SHA-pinning and minimal permissions
- Container image security: Trivy, Syft and signing with Cosign/Sigstore
- Dependency confusion and how to defend with private npm scopes
Anatomy of a Supply Chain Attack
Understanding how supply chain attacks work is the first step to defending against them. There are several main categories, each with distinct characteristics and attack vectors.
Typosquatting: the danger of a typo
Typosquatting exploits common typing mistakes. An attacker publishes
lodahs (instead of lodash), requst (instead of
request), or colerrs (instead of colors). In 2025,
security researchers identified a set of typosquatted packages designed to mimic popular
libraries that launched hidden terminals through postinstall scripts to
silently exfiltrate credentials.
The primary defense is careful name verification before installing any package. Tools like Snyk and Socket.dev automatically analyze name similarity with existing packages and flag potential typosquatting before installation.
Dependency Confusion: public vs private scopes
Dependency confusion (or namespace confusion) is a more sophisticated attack first documented by Alex Birsan in 2021. The attacker publishes on public npm a package with the same name as an internal private package of the target company, but with a higher version number. npm, by default, resolves the highest version from the public registry, causing the malicious package to replace the legitimate one.
Real Case: Large-Scale Dependency Confusion
In 2021, Alex Birsan compromised over 35 large companies including Microsoft, Apple, PayPal, and Shopify using this technique, earning more than $130,000 in bug bounties. The mechanism was simple: find internal package names in public configuration files (package.json, pyproject.toml) and publish them with version 9.9.9 on public registries.
Maintainer Account Takeover
The most insidious attack of 2025 demonstrated how targeted phishing against maintainers has become the preferred vector. Once a legitimate maintainer's account is compromised, the attacker publishes malicious versions of already-trusted packages. The community trusts the package, security tests pass, and the malicious code enters production.
Lockfile Poisoning
In a lockfile poisoning attack, a malicious contributor directly modifies the
package-lock.json or yarn.lock in a pull request to point to
compromised versions or different hashes than expected. If the review process does not
include lockfile verification, the malicious code passes unnoticed.
npm audit: Advanced Usage
npm audit is the starting point, but using it correctly requires more than
a simple run. Let's see how to integrate it effectively into the development workflow.
# Basic audit with JSON output for automated processing
npm audit --json
# Audit only production dependencies (excludes devDependencies)
npm audit --omit=dev
# Automatically fix patchable vulnerabilities
npm audit fix
# Fix including breaking changes (use with caution)
npm audit fix --force
# Audit with severity threshold: exit code 1 if critical found
npm audit --audit-level=critical
# Audit with moderate threshold
npm audit --audit-level=moderate
# Output format for CI/CD pipeline
npm audit --json | jq '.metadata.vulnerabilities'
For a project with many dependencies, the number of vulnerabilities can be high and difficult
to manage. An effective strategy is to configure a .npmrc file with project audit
policies, or use npm audit with an exceptions configuration file.
# Install npm-audit-resolver to manage exceptions
npm install -g npm-audit-resolver
# Interactive process to handle each vulnerability
audit-resolve
# Subsequent verification (uses saved exceptions)
audit-resolve --ci
# package.json scripts for safe CI
# package.json
{
"scripts": {
"audit:ci": "npm audit --audit-level=high --omit=dev",
"audit:full": "npm audit --json > audit-report.json",
"audit:check": "npm audit --audit-level=critical"
}
}
pnpm audit and yarn audit
If you use pnpm or yarn, the audit commands are similar but with some important differences. pnpm offers more granular control over dependencies, while yarn v2/v3 (Berry) has significantly improved hash management.
# pnpm audit
pnpm audit
pnpm audit --audit-level high
pnpm audit --prod # production only
# yarn audit (classic v1)
yarn audit
yarn audit --level high
# yarn audit (Berry v2/v3)
yarn npm audit
yarn npm audit --severity high
# JSON output for processing
pnpm audit --json | jq '.advisories | length'
Lockfile Integrity: The First Line of Defense
The lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml)
is fundamental for build reproducibility and security. Each entry in the lockfile includes
not only the exact version, but also the content hash of the package.
npm ci vs npm install: A Critical Distinction
In production and CI/CD, always use npm ci instead of npm install.
npm ci installs exactly the versions specified in the lockfile, verifies
the cryptographic hashes of each package, and fails if the lockfile is out-of-sync with
package.json. npm install can silently update the lockfile.
# CORRECT for CI/CD: verify lockfile integrity
npm ci
# Manual hash verification in the lockfile
# package-lock.json contains entries like:
# "node_modules/lodash": {
# "version": "4.17.21",
# "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
# "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZboqV76wE2wDvQ6",
# }
# Verify the lockfile hasn't been modified
git diff package-lock.json | head -50
# Pre-commit hook to prevent unauthorized lockfile changes
# .husky/pre-commit
#!/bin/sh
if git diff --cached --name-only | grep -q "package-lock.json"; then
echo "WARNING: package-lock.json modified. Verifying dependencies."
npm audit --audit-level=high
fi
.npmrc Configuration for Security
The .npmrc file allows you to configure npm with security policies that apply
to the entire project or the current user.
# .npmrc - project security configuration
# Always require HTTPS for the registry
registry=https://registry.npmjs.org/
# Enable strict-ssl (default: true, never disable!)
strict-ssl=true
# Automatic audit after every install
audit=true
# Fund messages: disable for CI
fund=false
# Use lockfile (default: true)
package-lock=true
# For workspaces with private packages on Artifactory/Nexus registry
# @mycompany:registry=https://npm.mycompany.internal/
# //npm.mycompany.internal/:_authToken={NPM_TOKEN}
# Dependency confusion prevention: scoped packages always on private registry
# @internal:registry=https://npm.internal.company.com/
Dependency Confusion: Defending with Private Scopes
The most effective defense against dependency confusion is ensuring that private packages always use a dedicated scope and that npm is configured to resolve that scope exclusively from the internal registry.
# package.json with correct private scope
{
"dependencies": {
"@mycompany/auth-utils": "^2.1.0",
"@mycompany/api-client": "^1.5.0"
}
}
# .npmrc - private scopes always on internal registry
@mycompany:registry=https://npm.mycompany.internal/
//npm.mycompany.internal/:_authToken=






