Skip to content

[Bug] compareVersions ranks a pre-release above its release, so a pre-release install before the fix is reported not vulnerable #1077

Description

@cnYui

Summary

compareVersions in src/utils/version.ts ranks a pre-release above its associated release, inverting semver §11.3 ("a pre-release version has lower precedence than the associated normal version"). Because versionMatchesRange in src/advisory/local-db.ts short-circuits on compareVersions(version, fixed) >= 0, an installed pre-release that sits before the fix version is silently classified as not vulnerable.

That is a false negative in a vulnerability scanner, so I am reporting it rather than sitting on it.

Root cause: parseVersionTuple splits on /[.+-]/ into one flat tuple, and compareVersions pads the shorter tuple with the number 0. A pre-release tag therefore ends up compared against 0 as a string, and "beta".localeCompare("0") is 1.

// src/utils/version.ts L28-38
const len = Math.max(pa.length, pb.length);
for (let i = 0; i < len; i++) {
  const av = pa[i] ?? 0;
  const bv = pb[i] ?? 0;
  ...

Pre-releases do reach the matcher: findMatchingVulnerabilityIds gates only on looksLikeVersion, whose regex ^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][^/]+)?$ explicitly admits them. isPreReleaseVersion is applied only to candidate fix versions in the remediation path, never to the installed version.

Environment

  • OS: Windows 11
  • Node.js version: v24.11.0
  • Package manager: npm 11.6.1
  • Lockfile type: n/a (defect is in the version comparator, reproducible directly)
  • CVE Lite CLI version: 1.33.0 (main @ 16d746c)

Command used

npx tsx repro.ts

Expected behavior

Per semver §11.3, 1.2.3-beta.1 < 1.2.3, so an advisory with fixed: "1.2.3" should still report an installed 1.2.3-beta.1 as vulnerable — the fix has not landed in that build.

Actual behavior

compareVersions("1.2.3-beta.1", "1.2.3") returns 1, and the advisory matcher reports 1.2.3-beta.1 as not vulnerable.

Reproduction

repro.ts at the repo root — it imports the real compareVersions / looksLikeVersion and inlines the exact body of versionMatchesRange (src/advisory/local-db.ts L215-233) so the matcher can be exercised without a sqlite database:

import { compareVersions, looksLikeVersion } from "./src/utils/version.js";

function versionMatchesRange(version: string, row: { introduced: string | null; fixed: string | null; last_affected: string | null }): boolean {
  const introduced = row.introduced;
  const fixed = row.fixed;
  const lastAffected = row.last_affected;
  if (introduced && introduced !== "0" && compareVersions(version, introduced) < 0) return false;
  if (fixed && compareVersions(version, fixed) >= 0) return false;
  if (lastAffected && compareVersions(version, lastAffected) > 0) return false;
  return true;
}

console.log(looksLikeVersion("1.2.3-beta.1"), "beta".localeCompare("0"));
console.log(compareVersions("1.2.3-beta.1", "1.2.3"));
const row = { introduced: "1.0.0", fixed: "1.2.3", last_affected: null };
for (const installed of ["1.2.2", "1.2.3-beta.1", "1.2.3-rc.2", "1.2.3"]) {
  console.log(installed, versionMatchesRange(installed, row));
}

Relevant files or output

looksLikeVersion('1.2.3-beta.1') = true
"beta".localeCompare("0")        = 1

compareVersions('1.2.3-beta.1','1.2.3') = 1   (semver 11.3 requires < 0)
compareVersions('1.2.3','1.2.3-beta.1') = -1  (semver 11.3 requires > 0)
compareVersions('2.0.0-rc.1','2.0.0')   = 1   (semver 11.3 requires < 0)

advisory { introduced: '1.0.0', fixed: '1.2.3' }
  installed 1.2.2          -> reported vulnerable: true
  installed 1.2.3-beta.1   -> reported vulnerable: false   <-- wrong, the fix is in 1.2.3
  installed 1.2.3-rc.2     -> reported vulnerable: false   <-- wrong
  installed 1.2.3          -> reported vulnerable: false

The same inversion reaches the online npm-advisory path at src/remediation/npm-registry.ts L404-408 (upperBoundOk), and affects compareVersions used as a sort comparator at L178 / L261-262.

Notes for whoever picks this up:

  • Nothing in tests/ pins the current behavior. The only pre-release assertion is tests/helpers.test.ts L351, compareVersions("1.2.3-beta", "1.2.3-alpha") > 0, which stays true under a semver-correct comparator.
  • Fixing it is contained to src/utils/version.ts; the two consumers become correct without edits.

I have a fix ready and will open a PR against this issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions