Skip to content
Node.js nd security 4 min read

Dependency Security & npm audit

A typical Node.js application ships far more third-party code than first-party code. Every package in node_modules—and every transitive dependency it pulls in—runs with the same privileges as your app, which makes the dependency tree one of the largest parts of your attack surface. Auditing those packages for known vulnerabilities, locking down what gets installed, and keeping the footprint small are the core defenses against supply-chain attacks.

Scanning with npm audit

npm audit cross-references your installed dependency tree against the npm advisory database and reports any package with a known CVE. It reads package-lock.json to resolve exact installed versions, so it reflects what is actually on disk, not just your declared ranges.

npm audit

Output:

# npm audit report

semver  <7.5.2
Severity: moderate
semver vulnerable to Regular Expression Denial of Service - https://github.com/advisories/GHSA-c2qf-rxjj-qqgw
fix available via `npm audit fix`
node_modules/semver

1 moderate severity vulnerability

To address all issues, run:
  npm audit fix

For CI pipelines, --json gives a machine-readable report and --audit-level lets you fail the build only at or above a chosen severity.

# Exit non-zero only if a high or critical issue exists
npm audit --audit-level=high

Run npm audit in CI, not just locally. A clean audit on your laptop means nothing if a vulnerable transitive dependency slips in through a teammate’s lockfile change.

Fixing vulnerabilities

npm audit fix upgrades vulnerable packages to the nearest non-breaking version that satisfies your declared semver ranges and updates the lockfile. When a fix requires a major version bump that could break your code, npm refuses to apply it automatically unless you opt in.

npm audit fix            # safe, in-range upgrades only
npm audit fix --force    # allows breaking major upgrades — review carefully

After --force, always run your full test suite. A “fix” that introduces a breaking API change is its own kind of outage. When a transitive dependency has no patched release yet, the overrides field in package.json lets you pin a known-good version across the whole tree.

{
  "overrides": {
    "semver": "7.5.4"
  }
}

Supply-chain attacks and lockfile integrity

Supply-chain attacks compromise the software you trust rather than your own code: a maintainer’s account is hijacked, a malicious version is published, or a typosquatted package (expres, lodahs) is installed by mistake. Your lockfile is the primary defense. package-lock.json records an integrity hash (a Subresource Integrity SHA-512 digest) for every package, so npm rejects any tarball whose contents don’t match what was locked.

Use npm ci instead of npm install in CI and production builds. It installs strictly from the lockfile, fails if package.json and the lockfile disagree, and never silently rewrites the tree.

npm ci

To reduce the window for malicious post-install scripts, you can disable lifecycle scripts during install and audit which packages even define them.

npm ci --ignore-scripts

Commit your lockfile. A repository without a committed package-lock.json resolves fresh versions on every install, defeating integrity checks and making builds non-reproducible.

Tooling: Snyk and Dependabot

npm audit is a baseline; dedicated tools add continuous monitoring, automated pull requests, and broader vulnerability databases.

ToolWhat it doesBest for
npm auditOn-demand scan against npm advisoriesQuick local and CI checks
DependabotGitHub-native; opens PRs for updates and alertsAutomated upgrades in GitHub repos
SnykDeep vuln DB, license checks, CI gatingOrg-wide policy and remediation

A minimal Dependabot config enables weekly dependency PRs:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"

Snyk can gate a pipeline and break the build when a new high-severity issue appears:

npx snyk test --severity-threshold=high

Minimizing the dependency footprint

The most reliable way to avoid vulnerable dependencies is to depend on fewer of them. Modern Node.js (20/22 LTS) ships capabilities that once required packages—native fetch, the built-in test runner, node:util parseArgs, and crypto—so reach for the standard library before adding a dependency.

import { parseArgs } from "node:util";
import { randomUUID } from "node:crypto";

// Native arg parsing and UUIDs — no third-party packages required
const { values } = parseArgs({
  options: { name: { type: "string" } },
});

console.log(`Hello ${values.name}, id ${randomUUID()}`);

Before adding any package, check how much it brings with it:

npm view express dependencies

Auditing what you already pull in is just as useful as auditing for CVEs:

# Find every package that defines a postinstall script
npm ls --all --json | grep -i postinstall

Best Practices

  • Run npm audit --audit-level=high in CI and fail the build on high or critical findings.
  • Use npm ci (not npm install) for reproducible, lockfile-verified installs in CI and production.
  • Always commit package-lock.json so integrity hashes are enforced on every install.
  • Prefer in-range npm audit fix; reserve --force for reviewed, tested major upgrades.
  • Enable Dependabot or Snyk for continuous monitoring instead of relying on manual scans.
  • Pin unpatched transitive dependencies with overrides until an upstream fix lands.
  • Favor the Node.js standard library over new packages to keep the dependency tree small.
Last updated June 14, 2026
Was this helpful?