Dependency Security: Auditing and Updating
A comprehensive guide to JavaScript dependency security covering npm audit, vulnerability remediation, supply chain protection, automated updates, and CI/CD security pipelines.
Dependency Security: Auditing and Updating
The average Node.js application pulls in hundreds of transitive dependencies. Every single one of them is a potential attack vector. If that does not concern you, it should.
In 2018, the event-stream incident proved how devastating supply chain attacks can be. A maintainer handed off a popular package to a stranger, who injected malicious code targeting a cryptocurrency wallet. The package had millions of weekly downloads. Nobody noticed for weeks. More recently, packages like ua-parser-js, coa, and rc were hijacked through compromised maintainer accounts, injecting credential-stealing malware into projects worldwide.
Your application is only as secure as its weakest dependency. This guide covers the tools, workflows, and mindset you need to keep your dependency tree clean.
Understanding the Threat Landscape
Supply chain attacks fall into several categories:
- Account takeover — An attacker compromises a maintainer's npm credentials and publishes a malicious version.
- Social engineering — An attacker gains commit access by offering to "help maintain" an abandoned package.
- Typosquatting — Publishing packages with names similar to popular ones (
lodahsinstead oflodash). - Dependency confusion — Exploiting private package name resolution to pull a malicious public package instead.
- Protestware — Maintainers intentionally sabotaging their own packages for political reasons (the
colorsandfakerincidents).
Traditional vulnerability scanning catches known CVEs. Supply chain protection goes further by detecting behavioral anomalies in packages — install scripts, network calls, filesystem access, and obfuscated code.
npm audit: Your First Line of Defense
Every Node.js developer should be running npm audit regularly. It checks your dependency tree against the GitHub Advisory Database and reports known vulnerabilities.
# Basic audit
npm audit
# JSON output for programmatic consumption
npm audit --json
# Only production dependencies (skip devDependencies)
npm audit --omit=dev
# Audit with specific severity threshold
npm audit --audit-level=high
Reading Audit Output
When you run npm audit, the output looks something like this:
# npm audit report
lodash <4.17.21
Severity: critical
Prototype Pollution - https://github.com/advisories/GHSA-jf85-cpcp-j695
fix available via `npm audit fix`
node_modules/lodash
nth-check <2.0.1
Severity: high
Inefficient Regular Expression Complexity - https://github.com/advisories/GHSA-rp65-9cf3-cjxr
fix available via `npm audit fix --force`
Will install [email protected], which is a breaking change
node_modules/nth-check
3 vulnerabilities (1 moderate, 1 high, 1 critical)
Each entry tells you the vulnerable package, the affected version range, severity, a link to the advisory, and whether a fix is available.
CVSS Scoring and Severity Levels
npm uses the Common Vulnerability Scoring System (CVSS) to classify severity:
| Severity | CVSS Score | Action |
|---|---|---|
| Critical | 9.0 - 10.0 | Fix immediately. Block deployments. |
| High | 7.0 - 8.9 | Fix within 24-48 hours. |
| Moderate | 4.0 - 6.9 | Fix within a sprint. |
| Low | 0.1 - 3.9 | Schedule for next maintenance window. |
Not all vulnerabilities are exploitable in your context. A ReDoS vulnerability in a server-side parsing library matters. The same vulnerability in a dev-only linting tool does not. Use judgment, but err on the side of patching.
Fixing Vulnerabilities
npm audit fix
The simplest approach:
# Apply non-breaking fixes
npm audit fix
# Apply all fixes, including breaking changes
npm audit fix --force
# Dry run to preview changes
npm audit fix --dry-run
A word of caution about --force: It will install major version bumps that may break your application. Never run npm audit fix --force and deploy without testing. I have seen it downgrade React from v18 to v16 to resolve a transitive dependency. Always do a dry run first.
npm overrides for Transitive Vulnerabilities
Sometimes the vulnerable package is deep in your dependency tree, and the direct dependency has not released a fix. This is where overrides in package.json save you:
{
"name": "my-app",
"version": "1.0.0",
"overrides": {
"nth-check": ">=2.0.1",
"lodash": ">=4.17.21",
"semver": ">=7.5.2"
}
}
Overrides force a specific version of a transitive dependency regardless of what the parent package requests. After adding overrides:
rm -rf node_modules package-lock.json
npm install
npm audit
Use overrides as a temporary measure. The proper fix is for the direct dependency to update, and you should open an issue or PR upstream.
When There Is No Fix
Sometimes a vulnerability has no patch available. Your options:
- Replace the dependency with an alternative that is actively maintained.
- Fork and patch the package yourself.
- Accept the risk if the vulnerability is not exploitable in your context, and document the decision.
- Use npm audit signatures to verify package provenance in the meantime.
Supply Chain Protection Tools
Snyk
Snyk goes beyond npm audit by providing continuous monitoring, fix PRs, and a broader vulnerability database:
# Install Snyk CLI
npm install -g snyk
# Authenticate
snyk auth
# Test your project
snyk test
# Monitor continuously (reports new vulnerabilities)
snyk monitor
# Test a specific package before installing
snyk test [email protected]
Snyk's database often catches vulnerabilities before they appear in the npm advisory database. The free tier covers open source projects well.
Socket.dev
Socket takes a fundamentally different approach. Instead of waiting for CVEs, it analyzes package behavior:
- Does the package execute install scripts?
- Does it access the network or filesystem?
- Has the maintainer changed recently?
- Is the code obfuscated?
- Has the package size changed dramatically between versions?
Install the Socket CLI or use their GitHub App for pull request analysis. It would have caught the event-stream attack because the behavioral change — adding a dependency that accesses process.env and makes network calls — would have triggered alerts.
Automated Dependency Updates
GitHub Dependabot
Dependabot automatically opens pull requests when new versions of your dependencies are available. Configure it with .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "America/Los_Angeles"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "automated"
reviewers:
- "your-team"
ignore:
- dependency-name: "aws-sdk"
update-types: ["version-update:semver-major"]
groups:
dev-dependencies:
dependency-type: "development"
update-types:
- "minor"
- "patch"
production-dependencies:
dependency-type: "production"
Grouping related updates into a single PR reduces noise. I group all dev dependency patches together because they rarely break anything individually.
Renovate
Renovate offers more flexibility than Dependabot. Configure it with renovate.json:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":automergeMinor",
":automergeDigest"
],
"packageRules": [
{
"matchDepTypes": ["devDependencies"],
"automerge": true,
"groupName": "dev dependencies"
},
{
"matchPackagePatterns": ["eslint"],
"groupName": "eslint"
},
{
"matchUpdateTypes": ["major"],
"labels": ["breaking-change"],
"automerge": false
}
],
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"]
}
}
Renovate can automerge patches and minor updates that pass CI, dramatically reducing the manual review burden.
Lock File Discipline
Your package-lock.json is a security artifact. It pins exact versions of every dependency in your tree, ensuring reproducible installs. Rules to live by:
- Always commit
package-lock.jsonto version control. - Use
npm ciin CI/CD instead ofnpm install. It installs exactly what the lock file specifies and fails if there is a mismatch. - Never
.gitignoreyour lock file. I have seen teams do this. It is a mistake. - Review lock file changes in PRs. A changed lock file can introduce unexpected dependency changes.
For deployment environments where you need even stricter control, use npm shrinkwrap. It creates an npm-shrinkwrap.json that is published with your package and takes precedence over package-lock.json:
npm shrinkwrap
This is particularly useful for CLI tools and libraries where you want downstream consumers to use your exact dependency tree.
Reviewing New Dependencies
Before running npm install some-package, do your due diligence:
# Check package metadata
npm info some-package
# See what files will be installed
npm pack some-package --dry-run
# Check download trends and maintenance
# Visit: https://npmtrends.com/some-package
# Check the package score
npx packagephobia some-package
Here is a script that automates pre-installation review:
// scripts/review-dependency.js
var https = require("https");
var packageName = process.argv[2];
if (!packageName) {
console.error("Usage: node review-dependency.js <package-name>");
process.exit(1);
}
function fetchJSON(url, callback) {
https.get(url, { headers: { "User-Agent": "dep-reviewer" } }, function(res) {
var data = "";
res.on("data", function(chunk) { data += chunk; });
res.on("end", function() {
try {
callback(null, JSON.parse(data));
} catch (err) {
callback(err);
}
});
}).on("error", callback);
}
function reviewPackage(name) {
var registryUrl = "https://registry.npmjs.org/" + name;
fetchJSON(registryUrl, function(err, data) {
if (err || data.error) {
console.error("Package not found:", name);
process.exit(1);
}
var latest = data["dist-tags"].latest;
var latestData = data.versions[latest];
var maintainers = data.maintainers || [];
var created = new Date(data.time.created);
var modified = new Date(data.time.modified);
var depCount = Object.keys(latestData.dependencies || {}).length;
var hasInstallScript = !!(latestData.scripts && (
latestData.scripts.preinstall ||
latestData.scripts.install ||
latestData.scripts.postinstall
));
console.log("\n=== Dependency Review: " + name + " ===\n");
console.log("Latest version: " + latest);
console.log("License: " + (latestData.license || "UNKNOWN"));
console.log("Created: " + created.toISOString().split("T")[0]);
console.log("Last modified: " + modified.toISOString().split("T")[0]);
console.log("Maintainers: " + maintainers.map(function(m) { return m.name; }).join(", "));
console.log("Dependencies: " + depCount);
console.log("Has install script:" + (hasInstallScript ? " YES (review carefully)" : " No"));
// Warnings
var warnings = [];
if (maintainers.length === 1) {
warnings.push("Single maintainer — bus factor risk");
}
if (depCount > 20) {
warnings.push("High dependency count (" + depCount + ") — larger attack surface");
}
if (hasInstallScript) {
warnings.push("Install scripts detected — potential supply chain risk");
}
var daysSinceUpdate = Math.floor((Date.now() - modified.getTime()) / 86400000);
if (daysSinceUpdate > 365) {
warnings.push("Not updated in " + daysSinceUpdate + " days — possibly abandoned");
}
if (!latestData.license) {
warnings.push("No license specified");
}
if (warnings.length > 0) {
console.log("\n--- WARNINGS ---");
warnings.forEach(function(w) { console.log(" ⚠ " + w); });
} else {
console.log("\nNo warnings detected.");
}
console.log("");
});
}
reviewPackage(packageName);
Run it before any new install:
node scripts/review-dependency.js express
License Compliance
Licenses matter, especially in commercial projects. Some licenses (GPL, AGPL) have copyleft requirements that may conflict with your business:
# Install license checker
npm install -g license-checker
# Check all dependency licenses
license-checker --summary
# Fail on specific licenses
license-checker --failOn "GPL-2.0;GPL-3.0;AGPL-3.0"
# Output as CSV for compliance review
license-checker --csv --out licenses.csv
Add license checking to your CI pipeline to prevent problematic licenses from slipping in.
Complete Working Example: CI Security Pipeline
Here is a GitHub Actions workflow that runs security checks on every pull request and deployment:
# .github/workflows/security-audit.yml
name: Dependency Security Audit
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
schedule:
- cron: "0 8 * * 1" # Every Monday at 8 AM UTC
jobs:
audit:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run npm audit
id: audit
run: |
npm audit --json > audit-results.json 2>&1 || true
node scripts/parse-audit.js audit-results.json
- name: Check for critical/high vulnerabilities
run: |
npm audit --audit-level=high
continue-on-error: ${{ github.event_name == 'schedule' }}
- name: License compliance check
run: |
npx license-checker --failOn "GPL-2.0;GPL-3.0;AGPL-3.0"
- name: Upload audit results
if: always()
uses: actions/upload-artifact@v4
with:
name: audit-results
path: audit-results.json
- name: Notify Slack on failure
if: failure() && github.ref == 'refs/heads/main'
uses: slackapi/[email protected]
with:
payload: |
{
"text": "Security audit failed on ${{ github.repository }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Security Audit Failed*\nRepo: `${{ github.repository }}`\nBranch: `${{ github.ref_name }}`\nCommit: `${{ github.sha }}`\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
And the local audit parsing script that provides human-readable output:
// scripts/parse-audit.js
var fs = require("fs");
var filePath = process.argv[2] || "audit-results.json";
var raw = fs.readFileSync(filePath, "utf8");
var audit;
try {
audit = JSON.parse(raw);
} catch (err) {
console.log("No vulnerabilities found or audit returned no JSON.");
process.exit(0);
}
var vulnerabilities = audit.vulnerabilities || {};
var packages = Object.keys(vulnerabilities);
if (packages.length === 0) {
console.log("No vulnerabilities found. All clear.");
process.exit(0);
}
var counts = { critical: 0, high: 0, moderate: 0, low: 0, info: 0 };
var details = [];
packages.forEach(function(name) {
var vuln = vulnerabilities[name];
var severity = vuln.severity || "info";
counts[severity] = (counts[severity] || 0) + 1;
details.push({
name: name,
severity: severity,
range: vuln.range || "unknown",
fixAvailable: vuln.fixAvailable ? "Yes" : "No",
via: Array.isArray(vuln.via)
? vuln.via.map(function(v) { return typeof v === "string" ? v : v.title || v.name; }).join(", ")
: "unknown"
});
});
// Sort by severity
var severityOrder = { critical: 0, high: 1, moderate: 2, low: 3, info: 4 };
details.sort(function(a, b) {
return (severityOrder[a.severity] || 5) - (severityOrder[b.severity] || 5);
});
console.log("\n========================================");
console.log(" DEPENDENCY SECURITY AUDIT REPORT");
console.log("========================================\n");
console.log("Summary:");
console.log(" Critical: " + counts.critical);
console.log(" High: " + counts.high);
console.log(" Moderate: " + counts.moderate);
console.log(" Low: " + counts.low);
console.log(" Info: " + counts.info);
console.log(" Total: " + packages.length);
console.log("\n----------------------------------------\n");
details.forEach(function(d) {
var tag = d.severity.toUpperCase();
while (tag.length < 8) tag += " ";
console.log("[" + tag + "] " + d.name);
console.log(" Range: " + d.range);
console.log(" Fix: " + d.fixAvailable);
console.log(" Via: " + d.via);
console.log("");
});
if (counts.critical > 0 || counts.high > 0) {
console.log("ACTION REQUIRED: " + (counts.critical + counts.high) + " critical/high vulnerabilities found.");
console.log("Run 'npm audit fix' or apply overrides for transitive dependencies.\n");
process.exit(1);
} else {
console.log("No critical or high vulnerabilities. Review moderate/low at your convenience.\n");
process.exit(0);
}
GitHub Security Advisories and Security Policies
Create a SECURITY.md file in your repository root:
# Security Policy
## Supported Versions
| Version | Supported |
|---------|-----------|
| 2.x | Yes |
| 1.x | Security patches only |
| < 1.0 | No |
## Reporting a Vulnerability
Please report vulnerabilities to [email protected].
Do NOT open a public GitHub issue for security vulnerabilities.
We will acknowledge receipt within 48 hours and provide
a detailed response within 5 business days.
Enable GitHub's security features in your repository settings: Dependabot alerts, Dependabot security updates, and code scanning. These are free for public repositories and available on GitHub Advanced Security for private ones.
Monitoring Production Dependencies
For production applications, runtime dependency monitoring adds another layer. Use npm ls to understand your dependency tree and identify bloat:
# Full dependency tree
npm ls --all
# Only production dependencies
npm ls --omit=dev
# Find duplicate packages
npm ls --all | grep "deduped" | wc -l
# Check for outdated packages
npm outdated
Build a regular cadence: audit weekly, update monthly, review quarterly. Automate what you can and reserve human attention for the decisions that need it.
Common Issues and Troubleshooting
1. npm audit fix Creates More Problems Than It Solves
Running npm audit fix --force can introduce breaking changes by upgrading or downgrading major versions. Always run --dry-run first. If the fix breaks your app, use overrides in package.json to pin the specific transitive dependency that needs patching without changing everything else.
2. Phantom Vulnerabilities in Dev Dependencies
You run npm audit and see 47 vulnerabilities, but 45 of them are in dev-only packages like react-scripts or webpack-dev-server. Use npm audit --omit=dev to focus on production risk. For CI pipelines that block on audit failures, audit only production dependencies to avoid false alarms.
3. Lock File Conflicts in Team Environments
Multiple developers running npm install with different Node.js or npm versions can generate conflicting lock files. Standardize your team on the same Node and npm versions using .nvmrc and engines in package.json:
{
"engines": {
"node": ">=20.0.0",
"npm": ">=10.0.0"
}
}
Use npm ci in all automated environments to ensure deterministic installs.
4. Overrides Not Taking Effect
If your overrides in package.json seem to be ignored, you likely have a stale node_modules or package-lock.json. Delete both and run npm install from scratch. Also verify that the override version satisfies the peer dependency requirements of the parent packages, otherwise npm may silently ignore it.
5. Audit Reporting Vulnerabilities With No Fix Available
This is frustrating but common. The advisory exists, but no patched version has been released. Check the GitHub issue on the advisory — sometimes a fix is merged but not yet published. In the meantime, evaluate whether the vulnerability is exploitable in your application context. If it is in a deeply nested dependency you do not directly use, the risk may be lower than the severity score suggests.
Best Practices
Run
npm auditin CI and fail the build on critical/high findings. Make security a gate, not an afterthought. Developers learn to keep dependencies clean when the build tells them to.Use
npm ciinstead ofnpm installin all automated environments. It is faster, deterministic, and catches lock file inconsistencies. There is no reason to usenpm installin CI.Review every new dependency before installing it. Check the maintainer, license, download count, last update, and dependency count. Five minutes of review can save weeks of incident response.
Automate dependency updates with Dependabot or Renovate. Configure automerge for patch and minor updates that pass CI. Only require manual review for major version bumps.
Keep your dependency count as low as possible. Every dependency is attack surface. Before adding a package, ask: can I write this in 20 lines of code? If yes, write it yourself. You do not need
is-odd.Pin dependencies in production applications and use ranges in libraries. Applications should have exact versions in the lock file. Libraries should use semver ranges to avoid version conflicts for consumers.
Separate security updates from feature work. Security patches should have their own PRs, their own review process, and their own fast-track to production. Do not bundle them with feature work that might delay deployment.
Audit your audit process quarterly. Tools evolve, new threats emerge, and team habits drift. Review your security pipeline, update your tooling, and verify that alerts are actually being acted on rather than ignored.