Skip to content

[Bug] satisfiesRange ignores the 0.x caret rule, so ^0.25.0 admits 0.26.0 and up #1177

Description

@MRX-72

Summary

satisfiesRange() treats ^ as "same major, >= floor". That's right for ^1.2.0 but wrong for 0.x: npm resolves ^0.25.0 to >=0.25.0 <0.26.0, because with a zero major the minor is the breaking position. The caret branch only compares majors, so 0 === 0 matches every 0.y.z above the floor.

src/utils/version.ts:223:

if (trimmedRange.startsWith("^")) {
  ...
  return vMajor === tMajor;   // true for every 0.x
}

isBreakingUpgrade() in the same file already handles the 0.x rule (from #990). satisfiesRange() never got it. The ~ branch is fine, it compares major and minor.

Actual vs expected

satisfiesRange("0.26.0", "^0.25.0") -> true    npm: false
satisfiesRange("0.99.0", "^0.25.0") -> true    npm: false
satisfiesRange("0.3.0",  "^0.2.3")  -> true    npm: false
satisfiesRange("0.0.4",  "^0.0.3")  -> true    npm: false
satisfiesRange("0.25.1", "^0.25.0") -> true    npm: true   (fine)

Why it matters

satisfiesRange is the semver oracle for four OA rules, and caret pins on 0.x are common in overrides (esbuild and its @esbuild/* platform binaries, vite plugins, most of the current toolchain).

The one that bites hardest is OA010. admitted-scan.ts:73 builds the admitted set with published.filter(v => satisfiesRange(v, range)), so for "esbuild": "^0.25.0" it admits every published 0.26.x, 0.27.x, ... and then reports "this floor admits vulnerable esbuild@0.2X.Y" for versions npm can never resolve under that override. False positive on a rule whose whole job is judging a floor.

Same wrong answer reaches OA006 (oa006:144, "installed copy is consistent with the pin"), OA005 (oa005:165) and OA008 (oa008:38), in the false-negative direction.

Reproduction

import { satisfiesRange } from "./src/utils/version.js";
satisfiesRange("0.26.0", "^0.25.0");  // true, npm says false

Fix

Take the ceiling from the leftmost non-zero component of the target: major > 0 -> same major; 0.y.z with y > 0 -> same major and minor; 0.0.z -> exact match.

tests/utils/version-extensions.test.ts only covers ^1.2.0, which is why this went unnoticed. Worth a 0.x case per branch.

Environment

Node v26.7.0, macOS, npm, cve-lite-cli 1.35.0 (main @ dad43cd). Pure version logic, no lockfile involved.


Separate but adjacent: the same function's doc comment promises pre-release exclusion it doesn't implement (coerceVersion strips the tag, so satisfiesRange("1.0.0-beta", "^1.0.0") is true; npm says false). Different branch, different fix. Say the word and I'll file it on its own.

Happy to pick this up if you want a PR.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    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