[Pkg-javascript-devel] Bug#987790: unblock: node-browserslist/4.16.3+~cs5.4.72-2

Yadd yadd at debian.org
Thu Apr 29 19:23:23 BST 2021


Package: release.debian.org
Severity: normal
User: release.debian.org at packages.debian.org
Usertags: unblock
X-Debbugs-Cc: pkg-javascript-devel at lists.alioth.debian.org

Please unblock package node-browserslist

[ Reason ]
node-browserslist is vulnerable to a Regex Denial of Service (ReDoS)
(CVE-2021-23364)

[ Impact ]
Medium vulnerability

[ Tests ]
I added a autopkgtest file to prove that CVE is fixed

[ Risks ]
Patch is a little big, I launched rebuilds to verify that all is OK:
rebuild      node-autoprefixer ... PASS
rebuild      node-babel7       ... PASS
rebuild      node-caniuse-api  ... PASS
rebuild      node-core-js      ... PASS
rebuild      node-jest         ... PASS
rebuild      node-katex        ... PASS

Of course autopkgtest is OK

[ Checklist ]
  [X] all changes are documented in the d/changelog
  [X] I reviewed all changes and I approve them
  [X] attach debdiff against the package in testing

Cheers,
Yadd

unblock node-browserslist/4.16.3+~cs5.4.72-2
-------------- next part --------------
diff --git a/debian/changelog b/debian/changelog
index ee4d58f..f53ddc3 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,12 @@
+node-browserslist (4.16.3+~cs5.4.72-2) unstable; urgency=medium
+
+  * Team upload
+  * Fix GitHub tags regex
+  * Fix ReDoS (Closes: CVE-2021-23364)
+  * Add CVE-2021-23364 test
+
+ -- Yadd <yadd at debian.org>  Thu, 29 Apr 2021 20:04:29 +0200
+
 node-browserslist (4.16.3+~cs5.4.72-1) unstable; urgency=medium
 
   * Team upload
diff --git a/debian/copyright b/debian/copyright
index 8f089e4..5166ddf 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -12,7 +12,7 @@ License: Expat
 
 Files: debian/*
 Copyright: 2017 Pirate Praveen <praveen at debian.org>
- 2020 Xavier Guimard <yadd at debian.org>
+ 2020 Yadd <yadd at debian.org>
 License: Expat
 
 Files: debian/tests/test_modules/*
diff --git a/debian/patches/CVE-2021-23364.patch b/debian/patches/CVE-2021-23364.patch
new file mode 100644
index 0000000..d02d08b
--- /dev/null
+++ b/debian/patches/CVE-2021-23364.patch
@@ -0,0 +1,391 @@
+Description: Fix ReDoS
+Author: Andrey Sitnik <andrey at sitnik.ru>
+ Yeting Li <liyt at ios.ac.cn>
+Origin: upstream, https://github.com/browserslist/browserslist/commit/c0919169
+ https://github.com/browserslist/browserslist/commit/433d5b8d
+Bug: https://snyk.io/vuln/SNYK-JS-BROWSERSLIST-1090194
+Forwarded: not-needed
+Reviewed-By: Yadd <yadd at debian.org>
+Last-Update: 2021-04-29
+
+--- a/index.js
++++ b/index.js
+@@ -614,6 +614,68 @@
+   }, 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 @@
+   {
+     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 @@
+     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,16 +787,19 @@
+     }
+   },
+   {
+-    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: /^(>=?|<=?)\s*(\d*\.?\d+)%$/,
++    regexp: /^since (\d+)-(\d+)$/i,
++    select: sinceQuery
++  },
++  {
++    regexp: /^since (\d+)-(\d+)-(\d+)$/i,
++    select: sinceQuery
++  },
++  {
++    regexp: /^(>=?|<=?)\s*(d+|\d*\.\d+)%$/,
+     select: function (context, sign, popularity) {
+       popularity = parseFloat(popularity)
+       var usage = browserslist.usage.global
+@@ -755,7 +824,7 @@
+     }
+   },
+   {
+-    regexp: /^(>=?|<=?)\s*(\d*\.?\d+)%\s+in\s+my\s+stats$/,
++    regexp: /^(>=?|<=?)\s*(d+|\d*\.\d+)%\s+in\s+my\s+stats$/,
+     select: function (context, sign, popularity) {
+       popularity = parseFloat(popularity)
+       if (!context.customUsage) {
+@@ -783,7 +852,7 @@
+     }
+   },
+   {
+-    regexp: /^(>=?|<=?)\s*(\d*\.?\d+)%\s+in\s+(\S+)\s+stats$/,
++    regexp: /^(>=?|<=?)\s*(d+|\d*\.\d+)%\s+in\s+(\S+)\s+stats$/,
+     select: function (context, sign, popularity, name) {
+       popularity = parseFloat(popularity)
+       var stats = env.loadStat(context, name, browserslist.data)
+@@ -818,7 +887,7 @@
+     }
+   },
+   {
+-    regexp: /^(>=?|<=?)\s*(\d*\.?\d+)%\s+in\s+((alt-)?\w\w)$/,
++    regexp: /^(>=?|<=?)\s*(d+|\d*\.\d+)%\s+in\s+((alt-)?\w\w)$/,
+     select: function (context, sign, popularity, place) {
+       popularity = parseFloat(popularity)
+       if (place.length === 2) {
+@@ -849,45 +918,12 @@
+     }
+   },
+   {
+-    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*\.\d+)%$/,
++    select: coverQuery
++  },
++  {
++    regexp: /^cover\s+(d+|\d*\.\d+)%\s+in\s+(my\s+stats|(alt-)?\w\w)$/,
++    select: coverQuery
+   },
+   {
+     regexp: /^supports\s+([\w-]+)$/,
+@@ -916,31 +952,26 @@
+       }
+       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 @@
+   {
+     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 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 @@
+     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 @@
+           return []
+         } else {
+           throw new BrowserslistError(
+-            'Unknown version ' + version + ' of ' + name)
++            'Unknown version ' + version + ' of ' + name
++          )
+         }
+       }
+       return [data.name + ' ' + version]
+@@ -1142,7 +1173,8 @@
+     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)
+       }
+--- a/test/node.test.ts
++++ b/test/node.test.ts
+@@ -25,14 +25,8 @@
+     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', () => {
diff --git a/debian/patches/series b/debian/patches/series
index 50c3e0b..3a3eedb 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1 +1,2 @@
 ignore-cross-spawn.patch
+CVE-2021-23364.patch
diff --git a/debian/tests/CVE-2021-23364.js b/debian/tests/CVE-2021-23364.js
new file mode 100644
index 0000000..c9feb97
--- /dev/null
+++ b/debian/tests/CVE-2021-23364.js
@@ -0,0 +1,34 @@
+var browserslist = require("browserslist")
+
+const startTime = Date.now();
+
+function build_attack(n) {
+    var ret = "> "
+    for (var i = 0; i < n; i++) {
+        ret += "1"
+    }
+    return ret + "!";
+}
+
+// browserslist('> 1%')
+
+//browserslist(build_attack(500000))
+for(var i = 1; i <= 500000; i++) {
+    if (i % 1000 == 0) {
+        var time = Date.now();
+        var attack_str = build_attack(i)
+        try{
+            browserslist(attack_str);
+            var time_cost = Date.now() - time;
+            console.log("attack_str.length: " + attack_str.length + ": " + time_cost+" ms");
+            }
+        catch(e){
+        var time_cost = Date.now() - time;
+        console.log("attack_str.length: " + attack_str.length + ": " + time_cost+" ms");
+        }
+    }
+    if(Date.now() - time > 5000) {
+        console.error('Vulnerable to CVE-2021-23364');
+        process.exit(1);
+    }
+}
diff --git a/debian/tests/control b/debian/tests/control
index ddead2a..7fa009c 100644
--- a/debian/tests/control
+++ b/debian/tests/control
@@ -1,3 +1,8 @@
 Test-Command: browserslist
 Depends: @
 Features: test-name=binary-test
+
+Test-Command: node debian/tests/CVE-2021-23364.js
+Depends: @
+Features: test-name=CVE-2021-23364
+Restrictions: superficial
diff --git a/debian/watch b/debian/watch
index 8d860bc..1ef219a 100644
--- a/debian/watch
+++ b/debian/watch
@@ -2,21 +2,21 @@ version=4
 opts=\
 dversionmangle=auto,\
 filenamemangle=s/.*\/v?([\d\.-]+)\.tar\.gz/node-browserslist-$1.tar.gz/ \
- https://github.com/ai/browserslist/tags .*/archive/v?([\d\.]+).tar.gz group
+ https://github.com/ai/browserslist/tags .*/archive/.*/v?([\d\.]+).tar.gz group
 
 opts=\
 component=node-releases,\
 dversionmangle=auto,\
 ctype=nodejs,\
 filenamemangle=s/.*\/v?([\d\.-]+)\.tar\.gz/node-node-releases-$1.tar.gz/ \
- https://github.com/chicoxyzzy/node-releases/tags .*/archive/v?([\d\.]+).tar.gz checksum
+ https://github.com/chicoxyzzy/node-releases/tags .*/archive/.*/v?([\d\.]+).tar.gz checksum
 
 opts=\
 component=colorette,\
 dversionmangle=auto,\
 ctype=nodejs,\
 filenamemangle=s/.*\/v?([\d\.-]+)\.tar\.gz/node-colorette-$1.tar.gz/ \
- https://github.com/jorgebucaran/colorette/tags .*/archive/v?([\d\.]+).tar.gz checksum
+ https://github.com/jorgebucaran/colorette/tags .*/archive/.*/v?([\d\.]+).tar.gz checksum
 
 # It is not recommended use npmregistry. Please investigate more.
 # Take a look at https://wiki.debian.org/debian/watch/


More information about the Pkg-javascript-devel mailing list