CVE-2021-23364 (JSON, published 2021-04-28T16:15Z)
Description: value: The package browserslist from 4.0.0 and before 4.16.5 are vulnerable to Regular Expression Denial of Service (ReDoS) during parsing of queries.
Severity: MEDIUM (CVSS v3 base score: 5.3)
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/browserslist/browserslist
Files changed:
index.js
package.json
test/node.test.ts
Commit message: Fix unsafe regexp


Vulnerable code:
      return Object.keys(e2c).slice(-versions).map(function (i) {
        return 'chrome ' + e2c[i]
      })
      return data.versions.filter(function (v) {
        return data.released.indexOf(v) === -1
      }).map(nameMapper(data.name))
    regexp: /^since (\d+)(?:-(\d+))?(?:-(\d+))?$/i,
    select: function (context, year, month, date) {
      year = parseInt(year)
      month = parseInt(month || '01') - 1
      date = parseInt(date || '01')
      return filterByYear(Date.UTC(year, month, date, 0, 0, 0), context)
    }
    regexp: /^cover\s+(\d*\.?\d+)%(\s+in\s+(my\s+stats|(alt-)?\w\w))?$/,
    select: function (context, coverage, statMode) {
      coverage = parseFloat(coverage)
      var usage = browserslist.usage.global
      if (statMode) {
        if (statMode.match(/^\s+in\s+my\s+stats$/)) {
          if (!context.customUsage) {
            throw new BrowserslistError(
              'Custom usage statistics was not provided'
            )
          }
          usage = context.customUsage
        } else {
          var match = statMode.match(/\s+in\s+((alt-)?\w\w)/)
          var place = match[1]
          if (place.length === 2) {
            place = place.toUpperCase()
          } else {
            place = place.toLowerCase()
          }
          env.loadCountry(browserslist.usage, place, browserslist.data)
          usage = browserslist.usage[place]
        }
      }
      var versions = Object.keys(usage).sort(function (a, b) {
        return usage[b] - usage[a]
      })
      var coveraged = 0
      var result = []
      var version
      for (var i = 0; i <= versions.length; i++) {
        version = versions[i]
        if (usage[version] === 0) break
        coveraged += usage[version]
        result.push(version)
        if (coveraged >= coverage) break
      }
      return result
    }
      return Object.keys(e2c).filter(function (i) {
        var parsed = parseFloat(i)
        return parsed >= from && parsed <= to
      }).map(function (i) {
        return 'chrome ' + e2c[i]
      })
      var nodeVersions = jsReleases.filter(function (i) {
        return i.name === 'nodejs'
      }).map(function (i) {
        return i.version
      })
      var semverRegExp = /^(0|[1-9]\d*)(\.(0|[1-9]\d*)){0,2}$/
      if (!semverRegExp.test(from)) {
        throw new BrowserslistError(
          'Unknown version ' + from + ' of Node.js')
      }
      if (!semverRegExp.test(to)) {
        throw new BrowserslistError(
          'Unknown version ' + to + ' of Node.js')
      }
      var nodeVersions = jsReleases.filter(function (i) {
        return i.name === 'nodejs'
      }).map(function (i) {
        return i.version
      })
          'Unknown version ' + version + ' of electron')
    regexp: /^node\s+(\d+(\.\d+)?(\.\d+)?)$/i,
    select: function (context, version) {
      var nodeReleases = jsReleases.filter(function (i) {
        return i.name === 'nodejs'
      })
      var matched = nodeReleases.filter(function (i) {
        return isVersionsMatch(i.version, version)
      })
      if (matched.length === 0) {
        if (context.ignoreUnknownVersions) {
          return []
        } else {
          throw new BrowserslistError(
            'Unknown version ' + version + ' of Node.js')
        }
      }
      return ['node ' + matched[matched.length - 1].version]
    }
      var queries = Object.keys(jsEOL).filter(function (key) {
        return now < Date.parse(jsEOL[key].end) &&
          now > Date.parse(jsEOL[key].start) &&
          isEolReleased(key)
      }).map(function (key) {
        return 'node ' + key.slice(1)
      })
            'Unknown version ' + version + ' of ' + name)
          'Specify versions in Browserslist query for browser ' + name)
      "security/detect-unsafe-regex": "off",
    }
    browserslist('node 6 - 8.a')
  }).toThrow(/Unknown/)
  expect(() => {
    browserslist('node 6.6.6.6 - 8')
  }).toThrow(/Unknown/)
  expect(() => {
    browserslist('node 6 - 8.01')
  }).toThrow(/Unknown/)

Fixed code:
function nodeQuery (context, version) {
  var nodeReleases = jsReleases.filter(function (i) {
    return i.name === 'nodejs'
  })
  var matched = nodeReleases.filter(function (i) {
    return isVersionsMatch(i.version, version)
  })
  if (matched.length === 0) {
    if (context.ignoreUnknownVersions) {
      return []
    } else {
      throw new BrowserslistError('Unknown version ' + version + ' of Node.js')
    }
  }
  return ['node ' + matched[matched.length - 1].version]
}

function sinceQuery (context, year, month, date) {
  year = parseInt(year)
  month = parseInt(month || '01') - 1
  date = parseInt(date || '01')
  return filterByYear(Date.UTC(year, month, date, 0, 0, 0), context)
}

function coverQuery (context, coverage, statMode) {
  coverage = parseFloat(coverage)
  var usage = browserslist.usage.global
  if (statMode) {
    if (statMode.match(/^my\s+stats$/)) {
      if (!context.customUsage) {
        throw new BrowserslistError(
          'Custom usage statistics was not provided'
        )
      }
      usage = context.customUsage
    } else {
      var place
      if (statMode.length === 2) {
        place = statMode.toUpperCase()
      } else {
        place = statMode.toLowerCase()
      }
      env.loadCountry(browserslist.usage, place, browserslist.data)
      usage = browserslist.usage[place]
    }
  }
  var versions = Object.keys(usage).sort(function (a, b) {
    return usage[b] - usage[a]
  })
  var coveraged = 0
  var result = []
  var version
  for (var i = 0; i <= versions.length; i++) {
    version = versions[i]
    if (usage[version] === 0) break
    coveraged += usage[version]
    result.push(version)
    if (coveraged >= coverage) break
  }
  return result
}

      return Object.keys(e2c)
        .slice(-versions)
        .map(function (i) {
          return 'chrome ' + e2c[i]
        })
      return data.versions
        .filter(function (v) {
          return data.released.indexOf(v) === -1
        })
        .map(nameMapper(data.name))
    regexp: /^since (\d+)$/i,
    select: sinceQuery
  },
  {
    regexp: /^since (\d+)-(\d+)$/i,
    select: sinceQuery
  },
  {
    regexp: /^since (\d+)-(\d+)-(\d+)$/i,
    select: sinceQuery
    regexp: /^cover\s+(\d*\.?\d+)%$/,
    select: coverQuery
  },
  {
    regexp: /^cover\s+(\d*\.?\d+)%\s+in\s+(my\s+stats|(alt-)?\w\w)$/,
    select: coverQuery
      return Object.keys(e2c)
        .filter(function (i) {
          var parsed = parseFloat(i)
          return parsed >= from && parsed <= to
        })
        .map(function (i) {
          return 'chrome ' + e2c[i]
        })
      var nodeVersions = jsReleases
        .filter(function (i) {
          return i.name === 'nodejs'
        })
        .map(function (i) {
          return i.version
        })
      var nodeVersions = jsReleases
        .filter(function (i) {
          return i.name === 'nodejs'
        })
        .map(function (i) {
          return i.version
        })
          'Unknown version ' + version + ' of electron'
        )
    regexp: /^node\s+(\d+)$/i,
    select: nodeQuery
  },
  {
    regexp: /^node\s+(\d+\.\d+)$/i,
    select: nodeQuery
  },
  {
    regexp: /^node\s+(\d+\.\d+\.\d+)$/i,
    select: nodeQuery
      var queries = Object.keys(jsEOL)
        .filter(function (key) {
          return (
            now < Date.parse(jsEOL[key].end) &&
            now > Date.parse(jsEOL[key].start) &&
            isEolReleased(key)
          )
        })
        .map(function (key) {
          return 'node ' + key.slice(1)
        })
            'Unknown version ' + version + ' of ' + name
          )
          'Specify versions in Browserslist query for browser ' + name
        )
    },
    "overrides": [
      {
        "files": "test/**/*",
        "rules": {
          "security/detect-unsafe-regex": "off"
        }
      }
    ]
    browserslist("node 6 - 8.a");
  }).toThrow(/Unknown/);