CVE-2022-25883 (JavaScript, published 2023-06-21T05:15Z)
Description: value: Versions of the package semver before 7.5.2 are vulnerable to Regular Expression Denial of Service (ReDoS) via the function new Range, when untrusted user data is provided as a range.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-1333 \u2014 Inefficient Regular Expression Complexity
The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles.
Repository: https://github.com/npm/node-semver
Files changed:
classes/comparator.js
classes/range.js
classes/semver.js
functions/coerce.js
internal/re.js
package.json
test/integration/whitespace.js
test/internal/re.js
test/map.js
Commit message: fix: better handling of whitespace (#564)



Vulnerable code:
const { re, t } = require('../internal/re')
    // First, split based on boolean or ||
    this.set = range
      .map(r => this.parseRange(r.trim()))
      throw new TypeError(`Invalid SemVer Range: ${range}`)
      .map((comps) => {
        return comps.join(' ').trim()
      })
    range = range.trim()

    // normalize spaces
    range = range.split(/\s+/).join(' ')

  re,
const replaceTildes = (comp, options) =>
  comp.trim().split(/\s+/).map((c) => {
    return replaceTilde(c, options)
  }).join(' ')
const replaceCarets = (comp, options) =>
  comp.trim().split(/\s+/).map((c) => {
    return replaceCaret(c, options)
  }).join(' ')
  return comp.split(/\s+/).map((c) => {
    return replaceXRange(c, options)
  }).join(' ')
  return comp.trim().replace(re[t.STAR], '')
  return comp.trim()
  return (`${from} ${to}`).trim()
const { re, t } = require('../internal/re')
const { re, t } = require('../internal/re')
    "check-coverage": true,
const { src, re } = require('../../internal/re')
// ensure that the coverage map maps all coverage
const ignore = [
  '.git',
  '.github',
  '.commitlintrc.js',
  '.eslintrc.js',
  '.eslintrc.local.js',
  'node_modules',
  'coverage',
  'tap-snapshots',
  'test',
  'fixtures',
]
const { statSync, readdirSync } = require('fs')
const find = (folder, set = [], root = true) => {
  const ent = readdirSync(folder)
  set.push(...ent.filter(f => !ignore.includes(f) && /\.m?js$/.test(f)).map(f => folder + '/' + f))
  for (const e of ent.filter(f => !ignore.includes(f) && !/\.m?js$/.test(f))) {
    if (statSync(folder + '/' + e).isDirectory()) {
      find(folder + '/' + e, set, false)
  if (!root) {
    return
  }
  return set.map(f => f.slice(folder.length + 1)
    .replace(/\\/g, '/'))
    .sort((a, b) => a.localeCompare(b))
const { resolve } = require('path')
const root = resolve(__dirname, '..')
const sut = find(root)
const tests = find(root + '/test')
t.strictSame(sut, tests, 'test files should match system files')
const map = require('../map.js')
for (const testFile of tests) {
  t.test(testFile, t => {
    t.plan(1)
    // cast to an array, since map() can return a string or array
    const systemFiles = [].concat(map(testFile))
    t.ok(systemFiles.some(sys => sut.includes(sys)), 'test covers a file')
  })
}

Fixed code:
    comp = comp.trim().split(/\s+/).join(' ')
const { safeRe: re, t } = require('../internal/re')
    // First reduce all whitespace as much as possible so we do not have to rely
    // on potentially slow regexes like \s*. This is then stored and used for
    // future error messages as well.
      .trim()
      .split(/\s+/)
      .join(' ')

    // First, split on ||
    this.set = this.raw
      .map(r => this.parseRange(r))
      throw new TypeError(`Invalid SemVer Range: ${this.raw}`)
      .map((comps) => comps.join(' ').trim())
  safeRe: re,
const replaceTildes = (comp, options) => {
  return comp
    .trim()
    .split(/\s+/)
    .map((c) => replaceTilde(c, options))
    .join(' ')
}
const replaceCarets = (comp, options) => {
  return comp
    .trim()
    .split(/\s+/)
    .map((c) => replaceCaret(c, options))
    .join(' ')
}
  return comp
    .split(/\s+/)
    .map((c) => replaceXRange(c, options))
    .join(' ')
  return comp
    .trim()
    .replace(re[t.STAR], '')
  return comp
    .trim()
  return `${from} ${to}`.trim()
const { safeRe: re, t } = require('../internal/re')
const { safeRe: re, t } = require('../internal/re')
const safeRe = exports.safeRe = []
  // 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')
  safeRe[index] = new RegExp(safe, isGlobal ? 'g' : undefined)
    "timeout": 30,
const { test } = require('tap')
const Range = require('../../classes/range')
const SemVer = require('../../classes/semver')
const Comparator = require('../../classes/comparator')
const validRange = require('../../ranges/valid')
const minVersion = require('../../ranges/min-version')
const minSatisfying = require('../../ranges/min-satisfying')
const maxSatisfying = require('../../ranges/max-satisfying')

const s = (n = 500000) => ' '.repeat(n)

test('regex dos via range whitespace', (t) => {
  // a range with this much whitespace 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`

  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('semver version', (t) => {
  const v = `${s(125)}1.2.3${s(125)}`
  const tooLong = `${s()}1.2.3${s()}`
  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')
  t.end()
})
const { src, re, safeRe } = require('../../internal/re')

  safeRe.forEach(r => {
    t.notMatch(r.source, '\\s+', 'safe regex do not contain greedy whitespace')
    t.notMatch(r.source, '\\s*', 'safe regex do not contain greedy whitespace')
  })

const { resolve, join, relative, extname, dirname, basename } = require('path')
const { statSync, readdirSync } = require('fs')
const map = require('../map.js')
const pkg = require('../package.json')
const ROOT = resolve(__dirname, '..')
const TEST = join(ROOT, 'test')
const IGNORE_DIRS = ['fixtures', 'integration']
const getFile = (f) => {
  try {
    if (statSync(f).isFile()) {
      return extname(f) === '.js' ? [f] : []
  } catch {
    return []
const walk = (item, res = []) => getFile(item) || readdirSync(item)
  .map(f => join(item, f))
  .reduce((acc, f) => acc.concat(statSync(f).isDirectory() ? walk(f, res) : getFile(f)), [])
  .filter(Boolean)
const walkAll = (items, relativeTo) => items
  .reduce((acc, f) => acc.concat(walk(join(ROOT, f))), [])
  .map((f) => relative(relativeTo, f))
  .sort()
t.test('tests match system', t => {
  const sut = walkAll([pkg.tap['coverage-map'], ...pkg.files], ROOT)
  const tests = walkAll([basename(TEST)], TEST)
    .filter(f => !IGNORE_DIRS.includes(dirname(f)))

  t.strictSame(sut, tests, 'test files should match system files')

  for (const f of tests) {
    t.test(f, t => {
      t.plan(1)
      t.ok(sut.includes(map(f)), 'test covers a file')
    })
  }

  t.end()
})