Package: release.debian.org Severity: normal User: release.debian....@packages.debian.org Usertags: unblock X-Debbugs-Cc: pkg-javascript-de...@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 <y...@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 <prav...@debian.org> - 2020 Xavier Guimard <y...@debian.org> + 2020 Yadd <y...@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 <and...@sitnik.ru> + Yeting Li <l...@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 <y...@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/