Skip to main content
BlogWeb Dev

npm Dependency Management and Security Auditing: What I Actually Do

How to audit npm dependencies for vulnerabilities, manage update debt, and protect your project from supply chain attacks.

Matthews Wong

Software Engineer

June 20259 min read
Web Dev

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.

npm audit: What It Tells You and What It Misses

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.

Reading Audit Output

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.

The Difference Between Direct and Transitive Dependencies

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 task

From 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.

Keeping Dependencies Updated

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.

Dependabot and Automated PRs

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.json

Supply Chain Security Beyond npm audit

For 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.

License Compliance

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.

My Weekly Dependency Hygiene Routine

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).

Sources & Further Reading

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is the difference between direct and transitive dependencies in a Next.js project?

Direct dependencies are packages you explicitly install and list in package.json, while transitive dependencies are packages required by those direct dependencies. A typical Next.js app has 30–50 direct dependencies but 500–1,000 transitive dependencies. Most vulnerabilities flagged by npm audit are in transitive packages you never installed directly, which is why they require special handling via upgrading the parent package or using npm's overrides field.

Why use --audit-level=high in CI instead of running npm audit with default settings?

The default audit level is low, which flags informational vulnerabilities and causes 'audit fatigue' — developers start ignoring output because items are always flagged. Running npm audit --audit-level=high makes CI fail only for high and critical vulnerabilities, giving alerts real meaning. Low and moderate vulnerabilities can then be reviewed weekly as a separate, non-blocking task.

Why is package-lock.json considered a security file and should always be committed?

The lockfile pins exact versions of every package in your dependency tree, including transitives. Without it, npm install resolves the latest version of each package at the time of installation, which means a deployment could pull in a newly published malicious version of a transitive dependency that was clean at development time. If your lockfile has unexpected changes after running npm install, you should investigate before merging — it could indicate dependency confusion or a compromised package.

What tools go beyond npm audit for supply chain security?

Socket.dev scans packages for suspicious behavior such as network access, process execution, and obfuscated code — behaviors that don't trigger CVEs and are invisible to npm audit. Snyk offers dependency scanning with more context and suggested remediation paths. OWASP's npm Security Cheat Sheet also recommends using npm ci instead of npm install in CI, since npm ci uses the lockfile exactly and fails if it is out of sync.

How should license compliance be handled as part of dependency management?

Licenses are a legal risk: a package with a GPL license in your dependency tree can impose copyleft requirements on your entire project. Tools like license-checker (CLI) or FOSSA (commercial, with CI integration) scan your dependencies for license types. For commercial projects, permissive licenses such as MIT, BSD, Apache-2.0, and ISC are preferable, while GPL, AGPL, and LGPL require extra scrutiny. Running a license scan should be part of your onboarding checklist when adding new dependencies.