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 auditin 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.jsonresolves 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.
| Tool | What it does | Best for |
|---|---|---|
npm audit | On-demand scan against npm advisories | Quick local and CI checks |
| Dependabot | GitHub-native; opens PRs for updates and alerts | Automated upgrades in GitHub repos |
| Snyk | Deep vuln DB, license checks, CI gating | Org-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=highin CI and fail the build on high or critical findings. - Use
npm ci(notnpm install) for reproducible, lockfile-verified installs in CI and production. - Always commit
package-lock.jsonso integrity hashes are enforced on every install. - Prefer in-range
npm audit fix; reserve--forcefor reviewed, tested major upgrades. - Enable Dependabot or Snyk for continuous monitoring instead of relying on manual scans.
- Pin unpatched transitive dependencies with
overridesuntil an upstream fix lands. - Favor the Node.js standard library over new packages to keep the dependency tree small.