diff --git a/classes/range.js b/classes/range.js index 53c2540f..a7d37203 100644 --- a/classes/range.js +++ b/classes/range.js @@ -98,15 +98,18 @@ class Range { const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE] range = range.replace(hr, hyphenReplace(this.options.includePrerelease)) debug('hyphen replace', range) + // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace) debug('comparator trim', range) // `~ 1.2.3` => `~1.2.3` range = range.replace(re[t.TILDETRIM], tildeTrimReplace) + debug('tilde trim', range) // `^ 1.2.3` => `^1.2.3` range = range.replace(re[t.CARETTRIM], caretTrimReplace) + debug('caret trim', range) // At this point, the range is completely trimmed and // ready to be split into comparators. diff --git a/internal/constants.js b/internal/constants.js index 25fab1ea..94be1c57 100644 --- a/internal/constants.js +++ b/internal/constants.js @@ -9,6 +9,10 @@ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || // Max safe segment length for coercion. const MAX_SAFE_COMPONENT_LENGTH = 16 +// Max safe length for a build identifier. The max length minus 6 characters for +// the shortest version with a build 0.0.0+BUILD. +const MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6 + const RELEASE_TYPES = [ 'major', 'premajor', @@ -22,6 +26,7 @@ const RELEASE_TYPES = [ module.exports = { MAX_LENGTH, MAX_SAFE_COMPONENT_LENGTH, + MAX_SAFE_BUILD_LENGTH, MAX_SAFE_INTEGER, RELEASE_TYPES, SEMVER_SPEC_VERSION, diff --git a/internal/re.js b/internal/re.js index f73ef1aa..9f5e36d5 100644 --- a/internal/re.js +++ b/internal/re.js @@ -1,4 +1,4 @@ -const { MAX_SAFE_COMPONENT_LENGTH } = require('./constants') +const { MAX_SAFE_COMPONENT_LENGTH, MAX_SAFE_BUILD_LENGTH } = require('./constants') const debug = require('./debug') exports = module.exports = {} @@ -9,16 +9,31 @@ const src = exports.src = [] const t = exports.t = {} let R = 0 +const LETTERDASHNUMBER = '[a-zA-Z0-9-]' + +// Replace some greedy regex tokens to prevent regex dos issues. These regex are +// used internally via the safeRe object since all inputs in this library get +// normalized first to trim and collapse all extra whitespace. The original +// regexes are exported for userland consumption and lower level usage. A +// future breaking change could export the safer regex only with a note that +// all input should have extra whitespace removed. +const safeRegexReplacements = [ + ['\\s', 1], + ['\\d', MAX_SAFE_COMPONENT_LENGTH], + [LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH], +] + +const makeSafeRegex = (value) => { + for (const [token, max] of safeRegexReplacements) { + value = value + .split(`${token}*`).join(`${token}{0,${max}}`) + .split(`${token}+`).join(`${token}{1,${max}}`) + } + return value +} + const createToken = (name, value, isGlobal) => { - // Replace all greedy whitespace to prevent regex dos issues. These regex are - // used internally via the safeRe object since all inputs in this library get - // normalized first to trim and collapse all extra whitespace. The original - // regexes are exported for userland consumption and lower level usage. A - // future breaking change could export the safer regex only with a note that - // all input should have extra whitespace removed. - const safe = value - .split('\\s*').join('\\s{0,1}') - .split('\\s+').join('\\s') + const safe = makeSafeRegex(value) const index = R++ debug(name, index, value) t[name] = index @@ -34,13 +49,13 @@ const createToken = (name, value, isGlobal) => { // A single `0`, or a non-zero digit followed by zero or more digits. createToken('NUMERICIDENTIFIER', '0|[1-9]\\d*') -createToken('NUMERICIDENTIFIERLOOSE', '[0-9]+') +createToken('NUMERICIDENTIFIERLOOSE', '\\d+') // ## Non-numeric Identifier // Zero or more digits, followed by a letter or hyphen, and then zero or // more letters, digits, or hyphens. -createToken('NONNUMERICIDENTIFIER', '\\d*[a-zA-Z-][a-zA-Z0-9-]*') +createToken('NONNUMERICIDENTIFIER', `\\d*[a-zA-Z-]${LETTERDASHNUMBER}*`) // ## Main Version // Three dot-separated numeric identifiers. @@ -75,7 +90,7 @@ createToken('PRERELEASELOOSE', `(?:-?(${src[t.PRERELEASEIDENTIFIERLOOSE] // ## Build Metadata Identifier // Any combination of digits, letters, or hyphens. -createToken('BUILDIDENTIFIER', '[0-9A-Za-z-]+') +createToken('BUILDIDENTIFIER', `${LETTERDASHNUMBER}+`) // ## Build Metadata // Plus sign, followed by one or more period-separated build metadata diff --git a/test/integration/whitespace.js b/test/integration/whitespace.js index eb9744d9..ae1451b1 100644 --- a/test/integration/whitespace.js +++ b/test/integration/whitespace.js @@ -7,33 +7,43 @@ const minVersion = require('../../ranges/min-version') const minSatisfying = require('../../ranges/min-satisfying') const maxSatisfying = require('../../ranges/max-satisfying') -const s = (n = 500000) => ' '.repeat(n) +const wsMedium = ' '.repeat(125) +const wsLarge = ' '.repeat(500000) +const zeroLarge = '0'.repeat(500000) -test('regex dos via range whitespace', (t) => { - // a range with this much whitespace would take a few minutes to process if +test('range with whitespace', (t) => { + // a range with these extra characters would take a few minutes to process if // any redos susceptible regexes were used. there is a global tap timeout per // file set in the package.json that will error if this test takes too long. - const r = `1.2.3 ${s()} <1.3.0` - + const r = `1.2.3 ${wsLarge} <1.3.0` t.equal(new Range(r).range, '1.2.3 <1.3.0') t.equal(validRange(r), '1.2.3 <1.3.0') t.equal(minVersion(r).version, '1.2.3') t.equal(minSatisfying(['1.2.3'], r), '1.2.3') t.equal(maxSatisfying(['1.2.3'], r), '1.2.3') + t.end() +}) +test('range with 0', (t) => { + const r = `1.2.3 ${zeroLarge} <1.3.0` + t.throws(() => new Range(r).range) + t.equal(validRange(r), null) + t.throws(() => minVersion(r).version) + t.equal(minSatisfying(['1.2.3']), null) + t.equal(maxSatisfying(['1.2.3']), null) t.end() }) test('semver version', (t) => { - const v = `${s(125)}1.2.3${s(125)}` - const tooLong = `${s()}1.2.3${s()}` + const v = `${wsMedium}1.2.3${wsMedium}` + const tooLong = `${wsLarge}1.2.3${wsLarge}` t.equal(new SemVer(v).version, '1.2.3') t.throws(() => new SemVer(tooLong)) t.end() }) test('comparator', (t) => { - const c = `${s()}<${s()}1.2.3${s()}` - t.equal(new Comparator(c).value, '<1.2.3') + const comparator = `${wsLarge}<${wsLarge}1.2.3${wsLarge}` + t.equal(new Comparator(comparator).value, '<1.2.3') t.end() })