CVE-2021-23364 (published 2021-04-28T16:15Z)

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 v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:N/A:P)
CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L)

Weakness information:
cwe_id: CWE-1333
cwe_name: Inefficient Regular Expression Complexity
description: The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles.
extended_description: Some regular expression engines have a feature called "backtracking". If the token cannot match, the engine "backtracks" to a position that may result in a different token that can match.
                Backtracking becomes a weakness if all of these conditions are met:
                
                    The number of possible backtracking attempts are exponential relative to the length of the input.
                    The input can fail to match the regular expression.
                    The input can be long enough.
                
		
		  Attackers can create crafted inputs that
		  intentionally cause the regular expression to use
		  excessive backtracking in a way that causes the CPU
		  consumption to spike.
		
            
            

Fix information:
repo_url: https://github.com/browserslist/browserslist
commit_details: repo_url: https://github.com/browserslist/browserslist
author: Andrey Sitnik
author_date: 2021-04-08 01:24:32-04:00
msg: Fix unsafe regexp
repository: repo_url: https://github.com/browserslist/browserslist
repo_name: browserslist/browserslist
description: 🦔 Share target browsers between different front-end tools, like Autoprefixer, Stylelint and babel-preset-env
repo_language: JavaScript
owner: browserslist
file_changes: filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -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)
       }

programming_language: JavaScript
filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -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"

programming_language: JavaScript
filename: node.test.ts
change_type: ModificationType.MODIFY
diff: @@ -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', () => {

programming_language: TypeScript