From 2124e4b155fb812d42985b289b549fab792356cd Mon Sep 17 00:00:00 2001 From: dualfroz Date: Thu, 3 Sep 2026 14:28:36 +0200 Subject: [PATCH] fix: reject loose ranges that collapse to match everything In loose mode the comparators that fail to parse are filtered out of a comparator list, and whatever is left becomes the range. When the only survivor is the any comparator, a typo such as `* (4.1.x 4.1.2)` drops `(4.1.x` and `4.1.2)` and collapses to the leftover `*`, so the range silently satisfies every version instead of being rejected. Without `loose` the same input throws `Invalid comparator: (4.1.x`. Treat that comparator list as invalid instead, the same way a list with no valid comparator at all is already treated: the list is dropped, so other `||` branches still stand on their own and a range left with no valid branch throws `Invalid SemVer Range`. Junk sitting next to a comparator that does parse is still tolerated, so `>=1.2.3 &&&` still yields `>=1.2.3` and `== 1.0.0` still yields `1.0.0`. Fixes #387 --- classes/range.js | 10 +++++++++- test/classes/range.js | 20 ++++++++++++++++++++ test/fixtures/range-parse.js | 8 ++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/classes/range.js b/classes/range.js index a7d6556..cc62092 100644 --- a/classes/range.js +++ b/classes/range.js @@ -143,10 +143,18 @@ class Range { if (loose) { // in loose mode, throw out any that are not valid comparators - rangeList = rangeList.filter(comp => { + const filtered = rangeList.filter(comp => { debug('loose invalid filter', comp, this.options) return !!comp.match(re[t.COMPARATORLOOSE]) }) + // dropping invalid comparators must not leave a range that matches + // everything: `* (4.1.x 4.1.2)` would collapse to the leftover `*`. + // if only the any comparator survives, treat the list as invalid. + if (filtered.length !== rangeList.length && + filtered.every(comp => comp === '')) { + return [] + } + rangeList = filtered } debug('range list', rangeList) diff --git a/test/classes/range.js b/test/classes/range.js index d012e50..4678a80 100644 --- a/test/classes/range.js +++ b/test/classes/range.js @@ -3,6 +3,7 @@ const { test } = require('tap') const Range = require('../../classes/range') const Comparator = require('../../classes/comparator') +const satisfies = require('../../functions/satisfies') const rangeIntersection = require('../fixtures/range-intersection.js') const rangeInclude = require('../fixtures/range-include.js') @@ -38,6 +39,25 @@ test('throw for empty comparator set, even in loose mode', t => { t.end() }) +test('loose mode does not drop comparators down to a match-everything range', t => { + // `(4.1.x` and `4.1.2)` used to be filtered out, collapsing the range to the + // `*` that was left over, so a typo silently matched every version + t.throws(() => new Range('* (4.1.x 4.1.2)', { loose: true }), + TypeError('Invalid SemVer Range: * (4.1.x 4.1.2)')) + t.equal(satisfies('9.9.9', '* (4.1.x 4.1.2)', { loose: true }), false, + 'typo range does not match an unrelated version') + t.equal(satisfies('4.1.2', '* (4.1.x 4.1.2)', { loose: true }), false, + 'typo range does not match the version it looks like it means') + // only the collapse to `*` is rejected. loose mode still tolerates junk + // sitting next to a comparator that does parse, and still tolerates a `||` + // branch that is invalid as a whole + t.equal(new Range('>=1.2.3 &&&', { loose: true }).range, '>=1.2.3', + 'junk beside a valid comparator is still dropped') + t.equal(new Range('1.2.3 || (foo)', { loose: true }).range, '1.2.3', + 'wholly invalid union branch is still tolerated') + t.end() +}) + test('convert comparator to range', t => { const c = new Comparator('>=1.2.3') const r = new Range(c) diff --git a/test/fixtures/range-parse.js b/test/fixtures/range-parse.js index 1c69c96..daab29f 100644 --- a/test/fixtures/range-parse.js +++ b/test/fixtures/range-parse.js @@ -148,4 +148,12 @@ module.exports = [ ['1.2.3+sha256.' + 'a'.repeat(200) + '.' + 'b'.repeat(200), '1.2.3', { loose: true }], ['1.2.3+' + 'a'.repeat(251) + ' || 2.0.0+' + 'b'.repeat(251), '1.2.3||2.0.0', { loose: true }], + // dropping unparseable comparators must not leave a match-everything range + ['* (4.1.x 4.1.2)', null], + ['* (4.1.x 4.1.2)', null, { loose: true }], + ['* (foo)', null, { loose: true }], + // but loose mode still tolerates junk next to a real comparator, and still + // tolerates a `||` branch that is invalid as a whole + ['>=1.2.3 &&&', '>=1.2.3', { loose: true }], + ['1.2.3 || (foo)', '1.2.3', { loose: true }], ]