On September 8, 2025, attackers compromised 18 widely used npm packages — including chalk, debug, ansi-styles, and strip-ansi — collectively downloaded over 2.6 billion times per week. The attack vector was a phishing campaign against a package maintainer. 80% of application dependencies remain un-upgraded for over a year, even when 95% of vulnerable components have fixed versions available. These statistics define the stakes: your supply chain is as secure as the least-secure maintainer of any package in your dependency tree. This is the audit and management process I run on every production Next.js project.
Running `npm audit` checks your installed packages against the npm advisory database — known vulnerabilities with published CVEs. It returns a list of vulnerabilities with severity ratings, affected package versions, and the recommended fix (usually upgrading to a patched version). The limitation: `npm audit` only catches known vulnerabilities with published advisories. It cannot detect zero-day exploits, malicious packages that behave as advertised, typosquatting attacks (packages with names similar to popular packages), or newly published malicious versions of legitimate packages.
npm audit output includes severity levels: critical (exploitable, high impact), high (exploitable, significant impact), moderate (requires unusual conditions), and low (minimal impact or requires chaining). For a production application, fix critical and high immediately. Moderate can be scheduled in the next sprint. Low can be batched. The `npm audit --json` flag outputs machine-readable JSON — pipe this to a script that filters by severity and fails CI if critical or high vulnerabilities exist.
Your package.json lists direct dependencies — packages you explicitly installed. Transitive dependencies are packages that your direct dependencies require. A typical Next.js app has 30-50 direct dependencies and 500-1,000 transitive dependencies. Most vulnerabilities flagged by `npm audit` are in transitive dependencies — packages you didn't explicitly install. Fixing them requires either upgrading the direct dependency that brings in the vulnerable transitive package, or using npm's `overrides` field to force a specific version of the transitive package.
npm Dependency Landscape (2025 reality):
────────────────────────────────────────────────────────────
Typical Next.js app:
package.json: ~40 direct dependencies
node_modules: ~800 total packages (transitives included)
↑ you didn't install 760 of these directly
Vulnerability sources:
Direct deps: You chose these → easy to update
Transitive: Pulled by your deps → harder to control
Sept 2025 supply chain attack:
Packages compromised: chalk, debug, ansi-styles, strip-ansi
Weekly downloads: 2.6 BILLION combined
Attack vector: Phishing against package maintainer
Detection: Not by npm audit (no CVE yet)
npm audit severity levels:
critical → Fix immediately (RCE, SQLi, auth bypass)
high → Fix this sprint
moderate → Schedule within 30 days
low → Batch with monthly updates
info → Review, may be false positive
npm audit --audit-level=high ← CI threshold I use
Fails CI only on high/critical → meaningful signal
Low/moderate reviewed weekly as separate taskFrom my experience managing Next.js project dependencies: run `npm audit --audit-level=high` in CI rather than just `npm audit`. The default audit level is low, which flags vulnerabilities that are typically informational and can lead to 'audit fatigue' — developers start ignoring the output because there are always flagged items. Setting --audit-level=high makes CI fail only for high and critical vulnerabilities, giving the alerts meaning. Then review low and moderate vulnerabilities weekly as a separate, non-blocking task.
The best defense against known vulnerabilities is staying current — most CVEs have a fixed version available. `npm outdated` lists packages with available updates. Use `npx npm-check-updates` (ncu) to update package.json to the latest versions (it won't break peer dependencies). The safe update process: run `ncu -u --target minor` to update minor versions (semver guarantees no breaking changes), test, then tackle major version updates one at a time. For a Next.js project, update `next`, `react`, and `react-dom` together as they have coordinated releases.
GitHub Dependabot automatically creates pull requests for dependency updates. Enable it by adding a .github/dependabot.yml file. Configure it to update the npm ecosystem weekly, group patch and minor updates together (to avoid 20 separate PRs per week), and update GitHub Actions separately. Dependabot PRs include the change type (patch/minor/major), a changelog link, and a compatibility score based on how many other repos successfully upgraded to the same version. Merge patch and minor updates with minimal review; review major updates manually.
# Weekly dependency audit workflow
npm audit --audit-level=high # check for critical/high CVEs
npm outdated # list packages with updates available
npx npm-check-updates -u --target minor # update minor versions in package.json
npm install # install updated packages
npm test && npm run e2e # verify nothing broke
# Fix transitive dependency vulnerability (can't update the parent yet):
# In package.json, use overrides:
{
"overrides": {
"vulnerable-transitive-package": "^2.1.0" // force safe version
}
}
# .github/dependabot.yml — automated PR for updates
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
day: monday
groups:
patch-and-minor:
patterns: ["*"]
update-types: ["minor", "patch"]
ignore:
- dependency-name: "@types/*"
update-types: ["version-update:semver-major"]
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: monthly
# CI — npm audit check in GitHub Actions
- name: Security audit
run: npm audit --audit-level=high
# Fails build if high or critical vulnerabilities found
# Use npm ci (not npm install) in CI — respects lockfile exactly
- name: Install dependencies
run: npm ci
# Fails if package-lock.json is out of sync with package.jsonFor higher-assurance projects, go beyond `npm audit`. Socket.dev scans packages for suspicious behavior (network access, process execution, obfuscated code) that doesn't trigger CVEs. Snyk offers dependency scanning with more context than npm audit, including suggested remediation paths. The OWASP npm Security Cheat Sheet recommends: locking transitive dependencies with a lockfile, using `npm ci` instead of `npm install` in CI (which uses the lockfile exactly and fails if it's out of sync), and enabling npm's built-in audit automation to block installs with known vulnerabilities.
The package-lock.json file pins the exact versions of every package in your dependency tree, including transitives. Without it (or yarn.lock, pnpm-lock.yaml), `npm install` resolves the latest version of each package — meaning a deployment could pull in a newly published malicious version of a transitive dependency that was clean at development time. Always commit your lockfile, and verify it's unchanged before deploying. If your lockfile has unexpected changes after running `npm install`, investigate before merging — it could indicate dependency confusion or a compromised package.
Security isn't the only dependency risk — licenses are a legal risk. A package with a GPL license in your dependency tree can impose GPL's copyleft requirements on your entire project. Use `license-checker` (CLI) or FOSSA (commercial, with CI integration) to scan your dependencies for license types. For a commercial project, you typically want MIT, BSD, Apache-2.0, and ISC licenses — they're permissive. Watch for GPL, AGPL, and LGPL. Run a license scan as part of your onboarding checklist when adding new dependencies.
What I actually do: Monday — run `npm audit` and check for new critical/high vulnerabilities (Dependabot handles this automatically via PR). Weekly — review and merge Dependabot PRs for patch and minor updates after CI passes. Monthly — run `ncu --target major` to see pending major upgrades, prioritize by usage frequency. Quarterly — full audit: license compliance, dependency count (are we adding more than we're removing?), and size impact of major dependencies using `bundlephobia`. Annually — consider replacing abandoned packages (no commits in 2+ years, unaddressed security reports).