Skip to content

Commit a8763f4

Browse files
committed
Improve redos protection, add many tests
100% line coverage, not quite 100% total though.
1 parent bafa295 commit a8763f4

9 files changed

+8163
-59
lines changed

minimatch.js

+74-45
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
module.exports = minimatch
22
minimatch.Minimatch = Minimatch
33

4-
var path = { sep: '/' }
5-
try {
6-
path = require('path')
7-
} catch (er) {}
4+
const path = (() => { try { return require('path') } catch (e) {}})() || {
5+
sep: '/'
6+
}
7+
minimatch.sep = path.sep
88

9-
var GLOBSTAR = minimatch.GLOBSTAR = Minimatch.GLOBSTAR = {}
10-
var expand = require('brace-expansion')
9+
const GLOBSTAR = minimatch.GLOBSTAR = Minimatch.GLOBSTAR = {}
10+
const expand = require('brace-expansion')
1111

12-
var plTypes = {
12+
const plTypes = {
1313
'!': { open: '(?:(?!(?:', close: '))[^/]*?)'},
1414
'?': { open: '(?:', close: ')?' },
1515
'+': { open: '(?:', close: ')+' },
@@ -19,22 +19,22 @@ var plTypes = {
1919

2020
// any single thing other than /
2121
// don't need to escape / when using new RegExp()
22-
var qmark = '[^/]'
22+
const qmark = '[^/]'
2323

2424
// * => any number of characters
25-
var star = qmark + '*?'
25+
const star = qmark + '*?'
2626

2727
// ** when dots are allowed. Anything goes, except .. and .
2828
// not (^ or / followed by one or two dots followed by $ or /),
2929
// followed by anything, any number of times.
30-
var twoStarDot = '(?:(?!(?:\\\/|^)(?:\\.{1,2})($|\\\/)).)*?'
30+
const twoStarDot = '(?:(?!(?:\\\/|^)(?:\\.{1,2})($|\\\/)).)*?'
3131

3232
// not a ^ or / followed by a dot,
3333
// followed by anything, any number of times.
34-
var twoStarNoDot = '(?:(?!(?:\\\/|^)\\.).)*?'
34+
const twoStarNoDot = '(?:(?!(?:\\\/|^)\\.).)*?'
3535

3636
// characters that need to be escaped in RegExp.
37-
var reSpecials = charSet('().*{}+?[]^$\\!')
37+
const reSpecials = charSet('().*{}+?[]^$\\!')
3838

3939
// "abc" -> { a:true, b:true, c:true }
4040
function charSet (s) {
@@ -45,7 +45,7 @@ function charSet (s) {
4545
}
4646

4747
// normalizes slashes.
48-
var slashSplit = /\/+/
48+
const slashSplit = /\/+/
4949

5050
minimatch.filter = filter
5151
function filter (pattern, options) {
@@ -58,41 +58,63 @@ function filter (pattern, options) {
5858
function ext (a, b) {
5959
a = a || {}
6060
b = b || {}
61-
var t = {}
62-
Object.keys(b).forEach(function (k) {
63-
t[k] = b[k]
64-
})
61+
const t = {}
6562
Object.keys(a).forEach(function (k) {
6663
t[k] = a[k]
6764
})
65+
Object.keys(b).forEach(function (k) {
66+
t[k] = b[k]
67+
})
6868
return t
6969
}
7070

7171
minimatch.defaults = function (def) {
72-
if (!def || !Object.keys(def).length) return minimatch
72+
if (!def || typeof def !== 'object' || !Object.keys(def).length) {
73+
return minimatch
74+
}
7375

74-
var orig = minimatch
76+
const orig = minimatch
7577

76-
var m = function minimatch (p, pattern, options) {
77-
return orig.minimatch(p, pattern, ext(def, options))
78+
const m = function minimatch (p, pattern, options) {
79+
return orig(p, pattern, ext(def, options))
7880
}
7981

8082
m.Minimatch = function Minimatch (pattern, options) {
8183
return new orig.Minimatch(pattern, ext(def, options))
8284
}
85+
m.Minimatch.defaults = options => {
86+
return orig.defaults(ext(def, options)).Minimatch
87+
}
88+
89+
m.filter = function filter (pattern, options) {
90+
return orig.filter(pattern, ext(def, options))
91+
}
92+
93+
m.defaults = function defaults (options) {
94+
return orig.defaults(ext(def, options))
95+
}
96+
97+
m.makeRe = function makeRe (pattern, options) {
98+
return orig.makeRe(pattern, ext(def, options))
99+
}
100+
101+
m.braceExpand = function braceExpand (pattern, options) {
102+
return orig.braceExpand(pattern, ext(def, options))
103+
}
104+
105+
m.match = function (list, pattern, options) {
106+
return orig.match(list, pattern, ext(def, options))
107+
}
83108

84109
return m
85110
}
86111

87112
Minimatch.defaults = function (def) {
88-
if (!def || !Object.keys(def).length) return Minimatch
89113
return minimatch.defaults(def).Minimatch
90114
}
91115

92116
function minimatch (p, pattern, options) {
93-
if (typeof pattern !== 'string') {
94-
throw new TypeError('glob pattern string required')
95-
}
117+
assertValidPattern(pattern)
96118

97119
if (!options) options = {}
98120

@@ -112,9 +134,7 @@ function Minimatch (pattern, options) {
112134
return new Minimatch(pattern, options)
113135
}
114136

115-
if (typeof pattern !== 'string') {
116-
throw new TypeError('glob pattern string required')
117-
}
137+
assertValidPattern(pattern)
118138

119139
if (!options) options = {}
120140
pattern = pattern.trim()
@@ -242,19 +262,27 @@ function braceExpand (pattern, options) {
242262
pattern = typeof pattern === 'undefined'
243263
? this.pattern : pattern
244264

245-
if (typeof pattern === 'undefined') {
246-
throw new TypeError('undefined pattern')
247-
}
265+
assertValidPattern(pattern)
248266

249-
if (options.nobrace ||
250-
!pattern.match(/\{.*\}/)) {
267+
if (options.nobrace || !/\{(?:(?!\{).)*\}/.test(pattern)) {
251268
// shortcut. no need to expand.
252269
return [pattern]
253270
}
254271

255272
return expand(pattern)
256273
}
257274

275+
const MAX_PATTERN_LENGTH = 1024 * 64
276+
const assertValidPattern = pattern => {
277+
if (typeof pattern !== 'string') {
278+
throw new TypeError('invalid pattern')
279+
}
280+
281+
if (pattern.length > MAX_PATTERN_LENGTH) {
282+
throw new TypeError('pattern is too long')
283+
}
284+
}
285+
258286
// parse a component of the expanded set.
259287
// At this point, no pattern may contain "/" in it
260288
// so we're going to return a 2d array, where each entry is the full
@@ -267,11 +295,9 @@ function braceExpand (pattern, options) {
267295
// of * is equivalent to a single *. Globstar behavior is enabled by
268296
// default, and can be disabled by setting options.noglobstar.
269297
Minimatch.prototype.parse = parse
270-
var SUBPARSE = {}
298+
const SUBPARSE = {}
271299
function parse (pattern, isSub) {
272-
if (pattern.length > 1024 * 64) {
273-
throw new TypeError('pattern is too long')
274-
}
300+
assertValidPattern(pattern)
275301

276302
var options = this.options
277303

@@ -280,7 +306,7 @@ function parse (pattern, isSub) {
280306
if (pattern === '') return ''
281307

282308
var re = ''
283-
var hasMagic = !!options.nocase
309+
var hasMagic = false
284310
var escaping = false
285311
// ? => one single character
286312
var patternListStack = []
@@ -332,10 +358,11 @@ function parse (pattern, isSub) {
332358
}
333359

334360
switch (c) {
335-
case '/':
361+
case '/': /* istanbul ignore next */ {
336362
// completely not allowed, even escaped.
337363
// Should already be path-split by now.
338364
return false
365+
}
339366

340367
case '\\':
341368
clearStateChar()
@@ -620,7 +647,7 @@ function parse (pattern, isSub) {
620647
var flags = options.nocase ? 'i' : ''
621648
try {
622649
var regExp = new RegExp('^' + re + '$', flags)
623-
} catch (er) {
650+
} catch (er) /* istanbul ignore next - should be impossible */ {
624651
// If it was an invalid regular expression, then it can't match
625652
// anything. This trick looks for a character after the end of
626653
// the string, which is of course impossible, except in multi-line
@@ -678,15 +705,15 @@ function makeRe () {
678705

679706
try {
680707
this.regexp = new RegExp(re, flags)
681-
} catch (ex) {
708+
} catch (ex) /* istanbul ignore next - should be impossible */ {
682709
this.regexp = false
683710
}
684711
return this.regexp
685712
}
686713

687714
minimatch.match = function (list, pattern, options) {
688715
options = options || {}
689-
var mm = new Minimatch(pattern, options)
716+
const mm = new Minimatch(pattern, options)
690717
list = list.filter(function (f) {
691718
return mm.match(f)
692719
})
@@ -779,6 +806,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) {
779806

780807
// should be impossible.
781808
// some invalid regexp stuff in the set.
809+
/* istanbul ignore if */
782810
if (p === false) return false
783811

784812
if (p === GLOBSTAR) {
@@ -852,6 +880,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) {
852880
// no match was found.
853881
// However, in partial mode, we can't say this is necessarily over.
854882
// If there's more *pattern* left, then
883+
/* istanbul ignore if */
855884
if (partial) {
856885
// ran out of file
857886
this.debug('\n>>> no match, partial?', file, fr, pattern, pr)
@@ -900,16 +929,16 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) {
900929
// this is ok if we're doing the match as part of
901930
// a glob fs traversal.
902931
return partial
903-
} else if (pi === pl) {
932+
} else /* istanbul ignore else */ if (pi === pl) {
904933
// ran out of pattern, still have file left.
905934
// this is only acceptable if we're on the very last
906935
// empty segment of a file with a trailing slash.
907936
// a/* should match a/b/
908-
var emptyFileEnd = (fi === fl - 1) && (file[fi] === '')
909-
return emptyFileEnd
937+
return (fi === fl - 1) && (file[fi] === '')
910938
}
911939

912940
// should be unreachable.
941+
/* istanbul ignore next */
913942
throw new Error('wtf?')
914943
}
915944

0 commit comments

Comments
 (0)