VYPR
Moderate severityNVD Advisory· Published Apr 28, 2021· Updated Sep 17, 2024

Regular Expression Denial of Service (ReDoS)

CVE-2021-23364

Description

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.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Browserslist before 4.16.5 is vulnerable to ReDoS during query parsing, enabling denial of service via crafted input.

Vulnerability

The package browserslist from version 4.0.0 and before 4.16.5 is vulnerable to Regular Expression Denial of Service (ReDoS) during parsing of user-supplied queries. The vulnerable code path resides in the regular expression used for parsing queries, which can lead to catastrophic backtracking when processing crafted input. The fix addresses this by tightening the regex to prevent excessive backtracking [1][2][4].

Exploitation

An attacker needs only the ability to supply a malicious query string to the browserslist API, such as through a web application or build tool that accepts user-defined browser queries. By crafting a specially designed input that triggers catastrophic backtracking in the vulnerable regex, the attacker can cause the Node.js process to consume excessive CPU time, effectively blocking the event loop and leading to a denial of service [3][4].

Impact

Successful exploitation leads to a denial of service condition. The affected process may become unresponsive or be terminated due to resource exhaustion, impacting the availability of applications or build pipelines that rely on browserslist for processing browser queries. There is no direct impact on confidentiality or integrity [3][4].

Mitigation

The vulnerability is fixed in version 4.16.5. Users should upgrade to 4.16.5 or later immediately. No known workarounds exist; the fix is available via the npm registry and the project's GitHub repository [1][2][4].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
browserslistnpm
>= 4.0.0, < 4.16.54.16.5

Affected products

3

Patches

1
c091916910df

Fix unsafe regexp

https://github.com/browserslist/browserslistAndrey SitnikApr 8, 2021via ghsa
3 files changed · +148 115
  • index.js+137 105 modified
    @@ -614,6 +614,68 @@ browserslist.coverage = function (browsers, stats) {
       }, 0)
     }
     
    +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
    +}
    +
     var QUERIES = [
       {
         regexp: /^last\s+(\d+)\s+major\s+versions?$/i,
    @@ -669,9 +731,11 @@ var QUERIES = [
       {
         regexp: /^last\s+(\d+)\s+electron\s+versions?$/i,
         select: function (context, versions) {
    -      return Object.keys(e2c).slice(-versions).map(function (i) {
    -        return 'chrome ' + e2c[i]
    -      })
    +      return Object.keys(e2c)
    +        .slice(-versions)
    +        .map(function (i) {
    +          return 'chrome ' + e2c[i]
    +        })
         }
       },
       {
    @@ -709,9 +773,11 @@ var QUERIES = [
         regexp: /^unreleased\s+(\w+)\s+versions?$/i,
         select: function (context, name) {
           var data = checkName(name, context)
    -      return data.versions.filter(function (v) {
    -        return data.released.indexOf(v) === -1
    -      }).map(nameMapper(data.name))
    +      return data.versions
    +        .filter(function (v) {
    +          return data.released.indexOf(v) === -1
    +        })
    +        .map(nameMapper(data.name))
         }
       },
       {
    @@ -721,13 +787,16 @@ var QUERIES = [
         }
       },
       {
    -    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: /^since (\d+)$/i,
    +    select: sinceQuery
    +  },
    +  {
    +    regexp: /^since (\d+)-(\d+)$/i,
    +    select: sinceQuery
    +  },
    +  {
    +    regexp: /^since (\d+)-(\d+)-(\d+)$/i,
    +    select: sinceQuery
       },
       {
         regexp: /^(>=?|<=?)\s*(\d*\.?\d+)%$/,
    @@ -849,45 +918,12 @@ var QUERIES = [
         }
       },
       {
    -    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
    -    }
    +    regexp: /^cover\s+(\d*\.?\d+)%$/,
    +    select: coverQuery
    +  },
    +  {
    +    regexp: /^cover\s+(\d*\.?\d+)%\s+in\s+(my\s+stats|(alt-)?\w\w)$/,
    +    select: coverQuery
       },
       {
         regexp: /^supports\s+([\w-]+)$/,
    @@ -916,31 +952,26 @@ var QUERIES = [
           }
           from = parseFloat(from)
           to = parseFloat(to)
    -      return Object.keys(e2c).filter(function (i) {
    -        var parsed = parseFloat(i)
    -        return parsed >= from && parsed <= to
    -      }).map(function (i) {
    -        return 'chrome ' + e2c[i]
    -      })
    +      return Object.keys(e2c)
    +        .filter(function (i) {
    +          var parsed = parseFloat(i)
    +          return parsed >= from && parsed <= to
    +        })
    +        .map(function (i) {
    +          return 'chrome ' + e2c[i]
    +        })
         }
       },
       {
         regexp: /^node\s+([\d.]+)\s*-\s*([\d.]+)$/i,
         select: function (context, from, to) {
    -      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
    +        })
           return nodeVersions
             .filter(semverFilterLoose('>=', from))
             .filter(semverFilterLoose('<=', to))
    @@ -976,11 +1007,13 @@ var QUERIES = [
       {
         regexp: /^node\s*(>=?|<=?)\s*([\d.]+)$/i,
         select: function (context, sign, version) {
    -      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
    +        })
           return nodeVersions
             .filter(generateSemverFilter(sign, version))
             .map(function (v) {
    @@ -1022,30 +1055,23 @@ var QUERIES = [
           var chrome = e2c[versionToUse]
           if (!chrome) {
             throw new BrowserslistError(
    -          'Unknown version ' + version + ' of electron')
    +          'Unknown version ' + version + ' of electron'
    +        )
           }
           return ['chrome ' + chrome]
         }
       },
       {
    -    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]
    -    }
    +    regexp: /^node\s+(\d+)$/i,
    +    select: nodeQuery
    +  },
    +  {
    +    regexp: /^node\s+(\d+\.\d+)$/i,
    +    select: nodeQuery
    +  },
    +  {
    +    regexp: /^node\s+(\d+\.\d+\.\d+)$/i,
    +    select: nodeQuery
       },
       {
         regexp: /^current\s+node$/i,
    @@ -1057,13 +1083,17 @@ var QUERIES = [
         regexp: /^maintained\s+node\s+versions$/i,
         select: function (context) {
           var now = Date.now()
    -      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)
    -      })
    +      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)
    +        })
           return resolve(queries, context)
         }
       },
    @@ -1100,7 +1130,8 @@ var QUERIES = [
               return []
             } else {
               throw new BrowserslistError(
    -            'Unknown version ' + version + ' of ' + name)
    +            'Unknown version ' + version + ' of ' + name
    +          )
             }
           }
           return [data.name + ' ' + version]
    @@ -1142,7 +1173,8 @@ var QUERIES = [
         select: function (context, name) {
           if (byName(name, context)) {
             throw new BrowserslistError(
    -          'Specify versions in Browserslist query for browser ' + name)
    +          'Specify versions in Browserslist query for browser ' + name
    +        )
           } else {
             throw unknownQuery(name)
           }
    
  • package.json+9 2 modified
    @@ -94,9 +94,16 @@
       "eslintConfig": {
         "extends": "@logux/eslint-config/browser",
         "rules": {
    -      "security/detect-unsafe-regex": "off",
           "global-require": "off"
    -    }
    +    },
    +    "overrides": [
    +      {
    +        "files": "test/**/*",
    +        "rules": {
    +          "security/detect-unsafe-regex": "off"
    +        }
    +      }
    +    ]
       },
       "eslintIgnore": [
         "test/fixtures"
    
  • test/node.test.ts+2 8 modified
    @@ -25,14 +25,8 @@ it('throws on malformed Node.js version', () => {
         browserslist('node 8.01')
       }).toThrow(/Unknown/)
       expect(() => {
    -    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/)
    +    browserslist("node 6 - 8.a");
    +  }).toThrow(/Unknown/);
     })
     
     it('return empty array on unknown Node.js version with special flag', () => {
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.