Skip to content

Commit 0572517

Browse files
committed
fix: better handling of whitespace (back-port to v5)
1 parent fd093cd commit 0572517

File tree

4 files changed

+100
-24
lines changed

4 files changed

+100
-24
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@
2323
"semver.js"
2424
],
2525
"tap": {
26-
"check-coverage": true
26+
"timeout": 30
2727
}
2828
}

semver.js

+42-23
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@ var MAX_SAFE_COMPONENT_LENGTH = 16
2828

2929
// The actual regexps go on exports.re
3030
var re = exports.re = []
31+
var safeRe = exports.safeRe = []
3132
var src = exports.src = []
3233
var R = 0
3334

35+
function makeSafeRe (value) {
36+
return value.split('\\s*').join('\\s{0,1}').split('\\s+').join('\\s')
37+
}
38+
3439
// The following Regular Expressions can be used for tokenizing,
3540
// validating, and parsing SemVer version strings.
3641

@@ -174,6 +179,7 @@ src[LONETILDE] = '(?:~>?)'
174179
var TILDETRIM = R++
175180
src[TILDETRIM] = '(\\s*)' + src[LONETILDE] + '\\s+'
176181
re[TILDETRIM] = new RegExp(src[TILDETRIM], 'g')
182+
safeRe[TILDETRIM] = new RegExp(makeSafeRe(src[TILDETRIM]), 'g')
177183
var tildeTrimReplace = '$1~'
178184

179185
var TILDE = R++
@@ -189,6 +195,7 @@ src[LONECARET] = '(?:\\^)'
189195
var CARETTRIM = R++
190196
src[CARETTRIM] = '(\\s*)' + src[LONECARET] + '\\s+'
191197
re[CARETTRIM] = new RegExp(src[CARETTRIM], 'g')
198+
safeRe[CARETTRIM] = new RegExp(makeSafeRe(src[CARETTRIM]), 'g')
192199
var caretTrimReplace = '$1^'
193200

194201
var CARET = R++
@@ -210,6 +217,7 @@ src[COMPARATORTRIM] = '(\\s*)' + src[GTLT] +
210217

211218
// this one has to use the /g flag
212219
re[COMPARATORTRIM] = new RegExp(src[COMPARATORTRIM], 'g')
220+
safeRe[COMPARATORTRIM] = new RegExp(makeSafeRe(src[COMPARATORTRIM]), 'g')
213221
var comparatorTrimReplace = '$1$2$3'
214222

215223
// Something like `1.2.3 - 1.2.4`
@@ -238,6 +246,14 @@ for (var i = 0; i < R; i++) {
238246
debug(i, src[i])
239247
if (!re[i]) {
240248
re[i] = new RegExp(src[i])
249+
250+
// Replace all greedy whitespace to prevent regex dos issues. These regex are
251+
// used internally via the safeRe object since all inputs in this library get
252+
// normalized first to trim and collapse all extra whitespace. The original
253+
// regexes are exported for userland consumption and lower level usage. A
254+
// future breaking change could export the safer regex only with a note that
255+
// all input should have extra whitespace removed.
256+
safeRe[i] = new RegExp(src[i].split('\\s*').join('\\s{0,1}').split('\\s+').join('\\s'))
241257
}
242258
}
243259

@@ -262,7 +278,7 @@ function parse (version, options) {
262278
return null
263279
}
264280

265-
var r = options.loose ? re[LOOSE] : re[FULL]
281+
var r = options.loose ? safeRe[LOOSE] : safeRe[FULL]
266282
if (!r.test(version)) {
267283
return null
268284
}
@@ -317,7 +333,7 @@ function SemVer (version, options) {
317333
this.options = options
318334
this.loose = !!options.loose
319335

320-
var m = version.trim().match(options.loose ? re[LOOSE] : re[FULL])
336+
var m = version.trim().match(options.loose ? safeRe[LOOSE] : safeRe[FULL])
321337

322338
if (!m) {
323339
throw new TypeError('Invalid Version: ' + version)
@@ -731,6 +747,7 @@ function Comparator (comp, options) {
731747
return new Comparator(comp, options)
732748
}
733749

750+
comp = comp.trim().split(/\s+/).join(' ')
734751
debug('comparator', comp, options)
735752
this.options = options
736753
this.loose = !!options.loose
@@ -747,7 +764,7 @@ function Comparator (comp, options) {
747764

748765
var ANY = {}
749766
Comparator.prototype.parse = function (comp) {
750-
var r = this.options.loose ? re[COMPARATORLOOSE] : re[COMPARATOR]
767+
var r = this.options.loose ? safeRe[COMPARATORLOOSE] : safeRe[COMPARATOR]
751768
var m = comp.match(r)
752769

753770
if (!m) {
@@ -861,17 +878,24 @@ function Range (range, options) {
861878
this.loose = !!options.loose
862879
this.includePrerelease = !!options.includePrerelease
863880

864-
// First, split based on boolean or ||
881+
// First reduce all whitespace as much as possible so we do not have to rely
882+
// on potentially slow regexes like \s*. This is then stored and used for
883+
// future error messages as well.
865884
this.raw = range
866-
this.set = range.split(/\s*\|\|\s*/).map(function (range) {
885+
.trim()
886+
.split(/\s+/)
887+
.join(' ')
888+
889+
// First, split based on boolean or ||
890+
this.set = this.raw.split('||').map(function (range) {
867891
return this.parseRange(range.trim())
868892
}, this).filter(function (c) {
869893
// throw out any that are not relevant for whatever reason
870894
return c.length
871895
})
872896

873897
if (!this.set.length) {
874-
throw new TypeError('Invalid SemVer Range: ' + range)
898+
throw new TypeError('Invalid SemVer Range: ' + this.raw)
875899
}
876900

877901
this.format()
@@ -890,28 +914,23 @@ Range.prototype.toString = function () {
890914

891915
Range.prototype.parseRange = function (range) {
892916
var loose = this.options.loose
893-
range = range.trim()
894917
// `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4`
895-
var hr = loose ? re[HYPHENRANGELOOSE] : re[HYPHENRANGE]
918+
var hr = loose ? safeRe[HYPHENRANGELOOSE] : safeRe[HYPHENRANGE]
896919
range = range.replace(hr, hyphenReplace)
897920
debug('hyphen replace', range)
898921
// `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5`
899-
range = range.replace(re[COMPARATORTRIM], comparatorTrimReplace)
900-
debug('comparator trim', range, re[COMPARATORTRIM])
922+
range = range.replace(safeRe[COMPARATORTRIM], comparatorTrimReplace)
923+
debug('comparator trim', range, safeRe[COMPARATORTRIM])
901924

902925
// `~ 1.2.3` => `~1.2.3`
903-
range = range.replace(re[TILDETRIM], tildeTrimReplace)
926+
range = range.replace(safeRe[TILDETRIM], tildeTrimReplace)
904927

905928
// `^ 1.2.3` => `^1.2.3`
906-
range = range.replace(re[CARETTRIM], caretTrimReplace)
907-
908-
// normalize spaces
909-
range = range.split(/\s+/).join(' ')
929+
range = range.replace(safeRe[CARETTRIM], caretTrimReplace)
910930

911931
// At this point, the range is completely trimmed and
912932
// ready to be split into comparators.
913-
914-
var compRe = loose ? re[COMPARATORLOOSE] : re[COMPARATOR]
933+
var compRe = loose ? safeRe[COMPARATORLOOSE] : safeRe[COMPARATOR]
915934
var set = range.split(' ').map(function (comp) {
916935
return parseComparator(comp, this.options)
917936
}, this).join(' ').split(/\s+/)
@@ -987,7 +1006,7 @@ function replaceTildes (comp, options) {
9871006
}
9881007

9891008
function replaceTilde (comp, options) {
990-
var r = options.loose ? re[TILDELOOSE] : re[TILDE]
1009+
var r = options.loose ? safeRe[TILDELOOSE] : safeRe[TILDE]
9911010
return comp.replace(r, function (_, M, m, p, pr) {
9921011
debug('tilde', comp, _, M, m, p, pr)
9931012
var ret
@@ -1028,7 +1047,7 @@ function replaceCarets (comp, options) {
10281047

10291048
function replaceCaret (comp, options) {
10301049
debug('caret', comp, options)
1031-
var r = options.loose ? re[CARETLOOSE] : re[CARET]
1050+
var r = options.loose ? safeRe[CARETLOOSE] : safeRe[CARET]
10321051
return comp.replace(r, function (_, M, m, p, pr) {
10331052
debug('caret', comp, _, M, m, p, pr)
10341053
var ret
@@ -1087,7 +1106,7 @@ function replaceXRanges (comp, options) {
10871106

10881107
function replaceXRange (comp, options) {
10891108
comp = comp.trim()
1090-
var r = options.loose ? re[XRANGELOOSE] : re[XRANGE]
1109+
var r = options.loose ? safeRe[XRANGELOOSE] : safeRe[XRANGE]
10911110
return comp.replace(r, function (ret, gtlt, M, m, p, pr) {
10921111
debug('xRange', comp, ret, gtlt, M, m, p, pr)
10931112
var xM = isX(M)
@@ -1157,10 +1176,10 @@ function replaceXRange (comp, options) {
11571176
function replaceStars (comp, options) {
11581177
debug('replaceStars', comp, options)
11591178
// Looseness is ignored here. star is always as loose as it gets!
1160-
return comp.trim().replace(re[STAR], '')
1179+
return comp.trim().replace(safeRe[STAR], '')
11611180
}
11621181

1163-
// This function is passed to string.replace(re[HYPHENRANGE])
1182+
// This function is passed to string.replace(safeRe[HYPHENRANGE])
11641183
// M, m, patch, prerelease, build
11651184
// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5
11661185
// 1.2.3 - 3.4 => >=1.2.0 <3.5.0 Any 3.4.x will do
@@ -1471,7 +1490,7 @@ function coerce (version) {
14711490
return null
14721491
}
14731492

1474-
var match = version.match(re[COERCE])
1493+
var match = version.match(safeRe[COERCE])
14751494

14761495
if (match == null) {
14771496
return null

test/re.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
var test = require('tap').test
2+
var semver = require('../')
3+
4+
test('has a list of src, re, and safeRe', function (t) {
5+
semver.re.forEach(function (r) { return t.match(r, RegExp, 'regexps are regexps') })
6+
semver.src.forEach(function (s) { return t.match(s, String, 'src is strings') })
7+
8+
semver.safeRe.forEach(function (r) {
9+
t.notMatch(r.source, '\\s+', 'safe regex do not contain greedy whitespace')
10+
t.notMatch(r.source, '\\s*', 'safe regex do not contain greedy whitespace')
11+
})
12+
13+
t.end()
14+
})

test/whitespace.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
var test = require('tap').test
2+
var semver = require('../')
3+
4+
var validRange = semver.validRange
5+
var SemVer = semver.SemVer
6+
var Range = semver.Range
7+
var Comparator = semver.Comparator
8+
var minVersion = semver.minVersion
9+
var minSatisfying = semver.minSatisfying
10+
var maxSatisfying = semver.maxSatisfying
11+
12+
function s(n) {
13+
return ' '.repeat(n || 500000)
14+
}
15+
16+
test('regex dos via range whitespace', function (t) {
17+
// a range with this much whitespace would take a few minutes to process if
18+
// any redos susceptible regexes were used. there is a global tap timeout per
19+
// file set in the package.json that will error if this test takes too long.
20+
var r = `1.2.3 ${s()} <1.3.0`
21+
22+
t.equal(new Range(r).range, '1.2.3 <1.3.0')
23+
t.equal(validRange(r), '1.2.3 <1.3.0')
24+
t.equal(minVersion(r).version, '1.2.3')
25+
t.equal(minSatisfying(['1.2.3'], r), '1.2.3')
26+
t.equal(maxSatisfying(['1.2.3'], r), '1.2.3')
27+
28+
t.end()
29+
})
30+
31+
test('semver version', function (t) {
32+
var v = `${s(125)}1.2.3${s(125)}`
33+
var tooLong = `${s()}1.2.3${s()}`
34+
t.equal(new SemVer(v).version, '1.2.3')
35+
t.throws(function () { return new SemVer(tooLong) })
36+
t.end()
37+
})
38+
39+
test('comparator', function (t) {
40+
var c = `${s()}<${s()}1.2.3${s()}`
41+
t.equal(new Comparator(c).value, '<1.2.3')
42+
t.end()
43+
})

0 commit comments

Comments
 (0)