[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#987790: marked as done (unblock: node-browserslist/4.16.3+~cs5.4.72-2)



Your message dated Sat, 01 May 2021 07:17:11 +0000
with message-id <E1lcjsF-0000XM-Vy@respighi.debian.org>
and subject line unblock node-browserslist
has caused the Debian Bug report #987790,
regarding unblock: node-browserslist/4.16.3+~cs5.4.72-2
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact owner@bugs.debian.org
immediately.)


-- 
987790: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=987790
Debian Bug Tracking System
Contact owner@bugs.debian.org with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
User: release.debian.org@packages.debian.org
Usertags: unblock
X-Debbugs-Cc: pkg-javascript-devel@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
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@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@debian.org>
- 2020 Xavier Guimard <yadd@debian.org>
+ 2020 Yadd <yadd@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@sitnik.ru>
+ Yeting Li <liyt@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@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/

--- End Message ---
--- Begin Message ---
Unblocked.

--- End Message ---

Reply to: