Back to glossary

npm audit

npm audit is the npm CLI command that checks a project's lockfile against GitHub advisories and reports dependencies with known vulnerabilities or malware.

Also known as npm audit fix, npm audit report, npm audit signatures, npm security audit, npm vulnerability scan

npm audit is the npm CLI command that checks every package version in a project's lockfile against the GitHub Advisory Database and reports known vulnerabilities by severity. It sends package names and version numbers to the npm registry, never code. Its subcommands, npm audit fix and npm audit signatures, upgrade flagged packages and verify registry signatures.

The most important fact about npm audit is when it runs. During npm install, npm sends the audit request in parallel with unpacking packages, runs their lifecycle scripts, and prints the report only after everything is on disk. A critical finding never stops an install. npm install exits 0 either way.

We think npm audit is good at exactly one job: telling you which versions in a lockfile match a published advisory. It is a useful inventory check and a poor supply chain defense. Malware reaches the advisory database hours or days after it has already executed, and only once someone publishes an advisory for it. The vulnerability findings are noisy, because severity scores know nothing about how your code uses a package.

What is npm audit?

#

npm audit is a lookup against a list of advisories, not an analysis of package code. It reads the dependency tree recorded in the lockfile, collects every package name and version, and POSTs that map to the registry's bulk advisory endpoint. Here is the whole request for a project with two dependencies, and the registry's answer for one of them, captured on October 30, 2026:

Bash
$ curl -s -X POST https://registry.npmjs.org/-/npm/v1/security/advisories/bulk \
    -H 'content-type: application/json' \
    -d '{"minimist":["1.2.5"],"lodash":["4.17.20"]}' | jq '.minimist'
[
  {
    "id": 1097678,
    "url": "https://github.com/advisories/GHSA-xvch-5gv4-984h",
    "title": "Prototype Pollution in minimist",
    "severity": "critical",
    "vulnerable_versions": ">=1.0.0 <1.2.6",
    "cwe": [
      "CWE-1321"
    ],
    "cvss": {
      "score": 9.8,
      "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
    }
  }
]

That request body is everything npm audit knows about your project. It carries no tarball hash, no file listing, and no install script. The registry matches each version against vulnerable_versions ranges taken from the GitHub Advisory Database and returns the hits. npm then marks meta-vulnerabilities, packages flagged only because they depend on a vulnerable range, and prints the path to each one.

The shape of that request explains three blind spots before we run anything:

  • An advisory must name an npm package and a version range to match. Many start without either. When the next-mdx-remote RCE was disclosed, its GitHub entry was “Unreviewed,” with no package listed, so npm audit had nothing to match against.
  • Matching is by name, not by origin. An internal package that shares a name with a public one inherits the public package's advisories, and the reverse. GitHub's advisory database documentation says most of its malware advisories concern substitution attacks, the dependency confusion pattern.
  • Anything outside the lockfile is invisible. npm requires a lockfile by default. Linked and workspace packages are left out of the payload. npm audit -g refuses to run with EAUDITGLOBAL: “npm audit does not support testing globals.”

One implementation detail changed in npm 12. npm 10.9 retries a failed bulk request against the older /-/npm/v1/security/audits/quick endpoint, which uploads the full lockfile tree plus the npm, Node.js, and platform versions. The installer in npm 12.2.1 has no such fallback. We confirmed both by reading the shipped source of each version.

Reading an npm audit report

#

Running npm audit in that two-dependency project with npm 10.9.9 prints one entry per vulnerable package:

Bash
$ npm audit
# npm audit report

lodash  <=4.17.23
Severity: high
Command Injection in lodash - https://github.com/advisories/GHSA-35jh-r3h4-6jhm
Regular Expression Denial of Service (ReDoS) in lodash - https://github.com/advisories/GHSA-29mw-wpgm-hmr9
lodash vulnerable to Code Injection via `_.template` imports key names - https://github.com/advisories/GHSA-r5fr-rjxr-66jc
lodash vulnerable to Prototype Pollution via array path bypass in `_.unset` and `_.omit` - https://github.com/advisories/GHSA-f23m-r3pf-42rh
Lodash has Prototype Pollution Vulnerability in `_.unset` and `_.omit` functions - https://github.com/advisories/GHSA-xxjr-mmjv-4gpg
fix available via `npm audit fix`
node_modules/lodash

minimist  1.0.0 - 1.2.5
Severity: critical
Prototype Pollution in minimist - https://github.com/advisories/GHSA-xvch-5gv4-984h
fix available via `npm audit fix`
node_modules/minimist

2 vulnerabilities (1 high, 1 critical)

To address all issues, run:
  npm audit fix
$ echo $?
1

Each entry lists the package, the vulnerable range, every matching advisory with its GHSA link, one severity, whether npm audit fix can resolve it, and where the package sits in node_modules. The severity comes from the advisory, usually from its CVSS score. npm audit exits 1 when any finding meets --audit-level, which defaults to every severity. The npm audit documentation notes that the flag only changes the failure threshold and does not filter the printed report. --omit=dev drops devDependencies from the request, and --json returns the same data as auditReportVersion 2.

Severity is also where npm audit gets noisy. An advisory scores the worst case for the library, not the way your code calls it. In 2021, React core team member Dan Abramov ran npm audit on a fresh Create React App and got 5 findings, 3 moderate and 2 high. He argued that none of them was a real risk for a build tool that outputs a static site. His conclusion: “Today, npm audit is broken by design.” (overreacted.io) Maintainers pay for the noise too. In the node-ip CVE dispute, CVE-2023-42282 was filed at CVSS 9.8, critical, and GitHub later cut it to low. In between, the maintainer temporarily archived the repository after users flooded him with messages about npm audit warnings.

We agree with that diagnosis. A list sorted by severity is a poor triage queue. Reachability analysis asks whether your code can actually call the vulnerable function, which is the question a severity score cannot answer.

When does npm audit run during npm install?

#

npm audit runs during npm install, in parallel with it, and its result never changes what gets installed. npm's installer, Arborist, sends the audit request as soon as the dependency tree is resolved. The source comment gives the reason: “so that it can overlap with the other actions as much as possible.” The installer then unpacks tarballs and runs lifecycle scripts, and waits for the audit result only at the very end.

Where npm audit runs inside npm install

Order of steps in npm's installer (Arborist reify), npm 10.9 and 12.2

Install
Audit request
1
Build the ideal treeRead the lockfile, resolve every name@version
2
Submit the auditPOST names and versions to /advisories/bulk, do not wait
sent
3
Unpack tarballsPackage code is now in node_modules
in flight
4
Run lifecycle scriptspreinstall, install, postinstall: default on through npm 11, allowlist only in npm 12
5
Save the lockfile
6
Await the audit result
used
7
Print “N vulnerabilities”Exit code 0, whatever the severity

npx: steps 1 to 6 run, step 7 is skipped. The request is sent and the answer is thrown away.

npm install -g: step 2 is skipped. Global installs are never audited.

The figure lists the seven install steps in order. Step 1 builds the dependency tree from the lockfile. Step 2 sends the audit request without waiting for it. Step 3 unpacks tarballs into node_modules. Step 4 runs preinstall, install, and postinstall scripts, which npm 11 and earlier do by default and npm 12 does only for packages on its allowScripts list. Step 5 saves the lockfile. Step 6 waits for the audit result. Step 7 prints the vulnerability count and exits 0. Under npx, steps 1 through 6 run and step 7 is skipped. Under npm install -g, step 2 is skipped.

npm's timing log shows the ordering. We installed a local package whose postinstall script writes a file, alongside minimist@1.2.5, with npm 10.9.9:

Bash
$ npm install ./evil minimist@1.2.5 --timing --loglevel=timing > install.log 2>&1
$ echo $?
0
$ grep -E 'reify:(unpack|build|audit)|postinstall|auditReport' install.log
npm timing reify:unpack Completed in 31ms
npm timing build:run:postinstall:evil Completed in 46ms
npm timing build:run:postinstall Completed in 46ms
npm timing reify:build Completed in 48ms
npm timing auditReport:getReport Completed in 88ms
npm timing auditReport:init Completed in 54ms
npm timing reify:audit Completed in 143ms

The postinstall script ran inside the build step and finished in 46 ms. The audit, sent before unpacking began, took 143 ms to complete, and the summary printed after both. npm install exited 0 with a critical advisory on minimist. Had that advisory been about malware, the payload would already have run.

npm v12 changed half of this. Published July 8, 2026, npm v12 turns dependency install scripts off by default through a new allowScripts setting, the same correction pnpm 10 made in January 2025. As of October 2026, npm 12.2.1 is the latest release. Default-on install scripts were a design mistake, and we are glad both package managers have reversed it. The fix covers scripts, not code. A package that runs on require, or ships inside a browser bundle, still executes later, and npm audit's verdict still arrives after the tarball is on disk. Node.js 22 also still bundles npm 10.9, so every machine on Node.js 22 keeps the old default until someone upgrades npm.

Two common paths skip the report entirely. npx installs the package into its cache, sends the same bulk request, and discards the answer. npm install -g never sends the request at all. Both runs below use npm 12.2.1 with an empty cache, and minimist@1.2.5 carries a critical advisory:

Bash
$ npx --yes -p minimist@1.2.5 -c 'echo ran'
npm notice run npx
npm notice run echo ran
ran

$ npm install -g --prefix /tmp/gpref minimist@1.2.5
added 1 package in 457ms

With --loglevel=silly, the npx run logs audit bulk request { minimist: [ '1.2.5' ] } and the critical response, then prints nothing to the user.

Does npm audit detect malware?

#

npm audit detects malware only after GitHub publishes a malware advisory for that exact package version. Those advisories exist, and the bulk endpoint returns them like any other finding:

Bash
$ curl -s -X POST https://registry.npmjs.org/-/npm/v1/security/advisories/bulk \
    -H 'content-type: application/json' \
    -d '{"plain-crypto-js":["4.2.1"],"axios":["1.14.1"],"chalk":["5.6.1"],"debug":["4.4.2"]}' \
  | jq -c '.[][] | select(.cwe | index("CWE-506")) | {title, severity, cvss: .cvss.score, vulnerable_versions}'
{"title":"Malware in plain-crypto-js","severity":"critical","cvss":0,"vulnerable_versions":">=0"}
{"title":"Malware in axios","severity":"critical","cvss":0,"vulnerable_versions":"=1.14.1"}
{"title":"Malware in chalk","severity":"critical","cvss":0,"vulnerable_versions":"=5.6.1"}
{"title":"debug@4.4.2 contains malware after npm account takeover","severity":"high","cvss":0,"vulnerable_versions":"=4.4.2"}

Three details in that response matter. Every malware advisory carries CWE-506, Embedded Malicious Code. The CVSS score is 0 everywhere, yet the severity labels disagree. chalk and debug were hijacked together in the Qix maintainer compromise and carried the same payload, but chalk is rated critical and debug only high. And none of these advisories existed when the malicious version was published:

Malicious versionPublished to npm (UTC)GitHub advisory publishedWhat happened in between
plain-crypto-js@4.2.12026-03-30 23:59:122026-03-31postinstall dropper for a remote access trojan. Socket flagged it at 00:05:41. npm replaced it with a security placeholder at 03:25:11.
axios@1.14.12026-03-31 00:21:582026-03-31Added plain-crypto-js@4.2.1 as a dependency. axios@0.30.4 followed at 01:00:57.
chalk@5.6.12025-09-08 13:13:052025-09-08Browser payload that swaps crypto wallet addresses. Clean 5.6.2 published at 14:47:54.
debug@4.4.22025-09-08 13:12:392025-09-15Same campaign. The advisory arrived 7 days later, with CVE-2025-59144.

Publish times come from each package's registry time field, read on October 30, 2026. Advisory dates come from GitHub's advisory pages, which give the day but not the hour.

Consider the first row as npm audit saw it. During the axios compromise, the registry served plain-crypto-js@4.2.1 for 3 hours and 26 minutes, and its postinstall hook ran a dropper that fetched a remote access trojan. Any npm install in that window ran the dropper, then received an audit report with nothing to say about it. When the advisory did arrive, its own text described an incident, not a prevention: “Any computer that has this package installed or running should be considered fully compromised.” (GHSA-fw8c-xr5c-95f9) Socket's automated malware detection, which analyzes what a package contains rather than waiting for an advisory feed, flagged plain-crypto-js@4.2.1 at 00:05:41 UTC, 16 minutes before axios@1.14.1 was even published.

Inconsistent severity breaks a natural CI gate. Some teams fail builds only on npm audit --audit-level=critical, to keep moderate noise from blocking merges. Against a lockfile that contains debug@4.4.2, that gate passes:

Bash
$ npm audit --audit-level=critical
# npm audit report

debug
Severity: high
debug@4.4.2 contains malware after npm account takeover - https://github.com/advisories/GHSA-4x49-vf9v-38px
No fix available
node_modules/debug

1 high severity vulnerability

Some issues need review, and may require choosing
a different dependency.
$ echo $?
0

The report names the package as malware, and the command still exits 0. Gate on the CWE instead of the severity label. This check fails the build on any CWE-506 advisory, whatever its severity:

Bash
npm audit --json \
  | jq -e '[.vulnerabilities[].via[] | objects | select(.cwe | index("CWE-506"))] | length == 0' \
  > /dev/null

The gate still knows only about malware that already has an advisory. Note the CVE column, too. chalk@5.6.1 has no CVE, and debug@4.4.2 got CVE-2025-59144 a week after the attack. Most malware never receives a CVE, so a scanner fed only by the CVE pipeline sees even less than npm audit does.

After an incident, npm audit is still worth running against old lockfiles. The registry has removed chalk@5.6.1 and debug@4.4.2, but a lockfile that recorded them is still flagged:

Bash
$ npm audit
# npm audit report

chalk
Severity: critical
Malware in chalk - https://github.com/advisories/GHSA-2v46-p5h4-248w
fix available via `npm audit fix`
node_modules/chalk

debug
Severity: high
debug@4.4.2 contains malware after npm account takeover - https://github.com/advisories/GHSA-4x49-vf9v-38px
fix available via `npm audit fix`

The range column is blank because the malicious versions no longer exist in the registry's metadata. npm also offers “fix available” for malware, as if it were a bug. A clean result proves less than it seems. Code fetched by npx, global installs, and throwaway CI containers never lands in the project lockfile. Our analysis of the hidden blast radius of the axios compromise put it this way: “The absence of evidence after the fact is not strong evidence of absence.”

What does npm audit fix do?

#

npm audit fix re-resolves the dependency tree while avoiding every vulnerable range, then runs a real install. It stays inside the semver ranges in package.json. --force lets it install versions outside those ranges, including major-version upgrades. --dry-run prints the plan without touching disk. Findings that cannot be fixed without changing a range report “No fix available” or ask for --force.

Because npm audit fix is an install, it carries the same risk as one. It fetches the newest acceptable versions, and on npm 11 and earlier it runs their install scripts. The newest release is exactly where a compromised version lands. An unattended npm audit fix --force in CI can pull an attacker's release minutes after it is published.

npm 12 adds a guard, and with it a conflict. With min-release-age set, the resolver ignores versions younger than the window, the dependency cooldown pattern. When that window blocks a security fix, npm audit fix leaves the package vulnerable, warns, and exits 1:

Bash
$ npm install lodash@4.17.21
$ npm audit fix --min-release-age=365
...
npm warn audit 1 package(s) left at a vulnerable version because a fix is newer than the release-age cutoff: lodash.
npm warn audit Add the package to min-release-age-exclude, or relax min-release-age or before, to install the fix.
$ echo $?
1

lodash's fix, 4.18.0, was published on March 31, 2026, inside the 365-day window. The escape hatch is min-release-age-exclude, set per package. We think npm made the right call by failing loudly here. A cooldown delays patches by exactly as many days as it delays attacks, so exempting a package should be a deliberate decision, not a default.

npm audit signatures checks who published, not what

#

npm audit signatures verifies the registry's ECDSA signatures on installed packages and any provenance attestations. A passing result proves that the tarball you have is the one the registry served. With provenance, it also proves which CI workflow built it. It never inspects the code. In the OpenAPI React Query Codegen compromise, all ten malicious versions carried valid provenance from the project's own GitHub Actions workflow. Our write-up warned: “Do not rely on npm audit signatures alone.” Provenance and signatures are necessary. They are not sufficient, because a compromised maintainer or workflow publishes through the legitimate pipeline.

Where npm audit fits in a pipeline

#

We keep npm audit, in its lane. It is a cheap, deterministic inventory check: given a lockfile, which versions match a published advisory. Run it in CI with an --audit-level you chose on purpose, add the CWE-506 gate above, and re-run it over old lockfiles when a new malware advisory lands.

npm audit is one of four checks that get confused with each other. They compare different things at different moments:

CheckWhat it comparesWhen it runsBlocks an install?What it misses
npm auditLockfile names and versions against published advisoriesDuring and after installNoMalware without an advisory yet; npx and global installs
npm audit signaturesTarball signatures and provenance against registry keysOn demand, after installNoMalicious code published through a legitimate account or workflow
Reachability analysisYour call graph against the vulnerable functionIn CI or on demandNoMalware, which needs no vulnerable function to be called
Package firewallEach requested package version against a malware verdictBefore the tarball is downloadedYesPackages already in the local cache; anything the verdict source has not flagged

Our verdict: stop malware with a package firewall before install, triage known vulnerabilities with npm audit plus reachability, and use npm audit signatures to confirm integrity. None of the four substitutes for another.

Prevention has to happen before step 3 in the figure, when the tarball is fetched. A package firewall sits at that point. Socket Firewall, for example, wraps the package manager (sfw npm install), runs it behind an ephemeral local proxy, and refuses the tarball request for known malicious versions, at any depth in the tree. It has limits of its own. It only sees network requests, so packages already in the local cache are not checked. The free version blocks only human-confirmed malware and merely warns on AI-detected suspects (docs). npm audit cannot block anything, and a firewall says nothing about the prototype pollution bug in the lodash version you installed in 2021.

The deeper problem is where npm audit learns about malware. In the plain-crypto-js case, the registry itself acted at 03:25:11 UTC when it swapped in a security placeholder, and npm audit learned only from a separate advisory published the same day. For debug@4.4.2, the advisory took a week. npm audit hears about malware only through that separate advisory record. Until registries publish takedowns as machine-readable advisories at the moment they happen, npm audit will keep reporting malware as history.