Skip to content

fix(arborist): allow npm audit fix to downgrade to a patched older version#9754

Open
tanistheta wants to merge 1 commit into
npm:latestfrom
tanistheta:fix/audit-fix-downgrade-vulnerable
Open

fix(arborist): allow npm audit fix to downgrade to a patched older version#9754
tanistheta wants to merge 1 commit into
npm:latestfrom
tanistheta:fix/audit-fix-downgrade-vulnerable

Conversation

@tanistheta

Copy link
Copy Markdown

Fixes #9557

Problem

npm audit reports fix available via npm audit fix, but running it silently does nothing. This happens when the only non-vulnerable version satisfying the declared range is older than what's installed, e.g. esbuild@^0.27.0 installed at 0.27.3, vulnerable range 0.27.3 – 0.28.0, only safe in-range version 0.27.2.

Root cause

AuditReport / pickManifest correctly resolve the safe older version (and classify it as an in-range, non-major fix - which is why the CLI advertises plain npm audit fix). But CanPlaceDep.checkCanPlaceCurrent() only ever replaces a current node with a semver-gte one:

const tryReplace = curVer && newVer && semver.gte(newVer, curVer)

The resolved fix is older, so it's discarded and the vulnerable node is kept via the KEEP fallback.

Fix

Thread auditReport through PlaceDep to CanPlaceDep and allow the replacement when the current node is vulnerable and the resolved one is not:

const securityDowngrade = !!(
  this.auditReport &&
  this.auditReport.isVulnerable(current) &&
  !this.auditReport.isVulnerable(dep)
)
const tryReplace = curVer && newVer && (semver.gte(newVer, curVer) || securityDowngrade)
  • dep.canReplace(current) still applies - a downgrade is never placed if it breaks any dependent.
  • A newer to older replacement path already exists here for preferDedupe; this adds a security-motivated trigger for the same REPLACE machinery.

Tests

  • 3 unit tests in test/can-place-dep.js (downgrade applied; no-vuln unchanged; still-vulnerable candidate not downgraded).
  • 1 integration test in test/arborist/audit.js through the full audit({fix: true}) flow - fails without the lib changes.
  • Both touched files at 100% coverage; all existing audit/placement suites pass.
  • Verified live against the real esbuild case from the issue: before up to date + vuln persists, after 0.27.2 + found 0 vulnerabilities.

when the only safe version in the declared range is older than what's installed, pickManifest resolved it correctly but CanPlaceDep discarded it, since it only ever replaced with semver-gte versions. thread the AuditReport through CanPlaceDep and allow the downgrade when the current node is vulnerable and the replacement is not.canReplace() still guards against breaking dependents.

fixes npm#9557
@tanistheta
tanistheta requested review from a team as code owners July 12, 2026 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] npm audit fix does nothing

1 participant