[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