Skip to content

Commit

Permalink
Improve redos protection, add many tests
Browse files Browse the repository at this point in the history
100% line coverage, not quite 100% total though.
  • Loading branch information
isaacs committed Feb 6, 2022
1 parent bafa295 commit a8763f4
Show file tree
Hide file tree
Showing 9 changed files with 8,163 additions and 59 deletions.
119 changes: 74 additions & 45 deletions minimatch.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
module.exports = minimatch
minimatch.Minimatch = Minimatch

var path = { sep: '/' }
try {
path = require('path')
} catch (er) {}
const path = (() => { try { return require('path') } catch (e) {}})() || {
sep: '/'
}
minimatch.sep = path.sep

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

var plTypes = {
const plTypes = {
'!': { open: '(?:(?!(?:', close: '))[^/]*?)'},
'?': { open: '(?:', close: ')?' },
'+': { open: '(?:', close: ')+' },
Expand All @@ -19,22 +19,22 @@ var plTypes = {

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

// * => any number of characters
var star = qmark + '*?'
const star = qmark + '*?'

This comment has been minimized.

Copy link
@invincibleRD

invincibleRD Nov 18, 2022

dcascca

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

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

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

// "abc" -> { a:true, b:true, c:true }
function charSet (s) {
Expand All @@ -45,7 +45,7 @@ function charSet (s) {
}

// normalizes slashes.
var slashSplit = /\/+/
const slashSplit = /\/+/

minimatch.filter = filter
function filter (pattern, options) {
Expand All @@ -58,41 +58,63 @@ function filter (pattern, options) {
function ext (a, b) {
a = a || {}
b = b || {}
var t = {}
Object.keys(b).forEach(function (k) {
t[k] = b[k]
})
const t = {}
Object.keys(a).forEach(function (k) {
t[k] = a[k]
})
Object.keys(b).forEach(function (k) {
t[k] = b[k]
})
return t
}

minimatch.defaults = function (def) {
if (!def || !Object.keys(def).length) return minimatch
if (!def || typeof def !== 'object' || !Object.keys(def).length) {
return minimatch
}

var orig = minimatch
const orig = minimatch

var m = function minimatch (p, pattern, options) {
return orig.minimatch(p, pattern, ext(def, options))
const m = function minimatch (p, pattern, options) {
return orig(p, pattern, ext(def, options))
}

m.Minimatch = function Minimatch (pattern, options) {
return new orig.Minimatch(pattern, ext(def, options))
}
m.Minimatch.defaults = options => {
return orig.defaults(ext(def, options)).Minimatch
}

m.filter = function filter (pattern, options) {
return orig.filter(pattern, ext(def, options))
}

m.defaults = function defaults (options) {
return orig.defaults(ext(def, options))
}

m.makeRe = function makeRe (pattern, options) {
return orig.makeRe(pattern, ext(def, options))
}

m.braceExpand = function braceExpand (pattern, options) {
return orig.braceExpand(pattern, ext(def, options))
}

m.match = function (list, pattern, options) {
return orig.match(list, pattern, ext(def, options))
}

return m
}

Minimatch.defaults = function (def) {
if (!def || !Object.keys(def).length) return Minimatch
return minimatch.defaults(def).Minimatch
}

function minimatch (p, pattern, options) {
if (typeof pattern !== 'string') {
throw new TypeError('glob pattern string required')
}
assertValidPattern(pattern)

if (!options) options = {}

Expand All @@ -112,9 +134,7 @@ function Minimatch (pattern, options) {
return new Minimatch(pattern, options)
}

if (typeof pattern !== 'string') {
throw new TypeError('glob pattern string required')
}
assertValidPattern(pattern)

if (!options) options = {}
pattern = pattern.trim()
Expand Down Expand Up @@ -242,19 +262,27 @@ function braceExpand (pattern, options) {
pattern = typeof pattern === 'undefined'
? this.pattern : pattern

if (typeof pattern === 'undefined') {
throw new TypeError('undefined pattern')
}
assertValidPattern(pattern)

if (options.nobrace ||
!pattern.match(/\{.*\}/)) {
if (options.nobrace || !/\{(?:(?!\{).)*\}/.test(pattern)) {
// shortcut. no need to expand.
return [pattern]
}

return expand(pattern)
}

const MAX_PATTERN_LENGTH = 1024 * 64
const assertValidPattern = pattern => {
if (typeof pattern !== 'string') {
throw new TypeError('invalid pattern')
}

if (pattern.length > MAX_PATTERN_LENGTH) {
throw new TypeError('pattern is too long')
}
}

// parse a component of the expanded set.
// At this point, no pattern may contain "/" in it
// so we're going to return a 2d array, where each entry is the full
Expand All @@ -267,11 +295,9 @@ function braceExpand (pattern, options) {
// of * is equivalent to a single *. Globstar behavior is enabled by
// default, and can be disabled by setting options.noglobstar.
Minimatch.prototype.parse = parse
var SUBPARSE = {}
const SUBPARSE = {}
function parse (pattern, isSub) {
if (pattern.length > 1024 * 64) {
throw new TypeError('pattern is too long')
}
assertValidPattern(pattern)

var options = this.options

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

var re = ''
var hasMagic = !!options.nocase
var hasMagic = false
var escaping = false
// ? => one single character
var patternListStack = []
Expand Down Expand Up @@ -332,10 +358,11 @@ function parse (pattern, isSub) {
}

switch (c) {
case '/':
case '/': /* istanbul ignore next */ {
// completely not allowed, even escaped.
// Should already be path-split by now.
return false
}

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

try {
this.regexp = new RegExp(re, flags)
} catch (ex) {
} catch (ex) /* istanbul ignore next - should be impossible */ {
this.regexp = false
}
return this.regexp
}

minimatch.match = function (list, pattern, options) {
options = options || {}
var mm = new Minimatch(pattern, options)
const mm = new Minimatch(pattern, options)
list = list.filter(function (f) {
return mm.match(f)
})
Expand Down Expand Up @@ -779,6 +806,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) {

// should be impossible.
// some invalid regexp stuff in the set.
/* istanbul ignore if */
if (p === false) return false

if (p === GLOBSTAR) {
Expand Down Expand Up @@ -852,6 +880,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) {
// no match was found.
// However, in partial mode, we can't say this is necessarily over.
// If there's more *pattern* left, then
/* istanbul ignore if */
if (partial) {
// ran out of file
this.debug('\n>>> no match, partial?', file, fr, pattern, pr)
Expand Down Expand Up @@ -900,16 +929,16 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) {
// this is ok if we're doing the match as part of
// a glob fs traversal.
return partial
} else if (pi === pl) {
} else /* istanbul ignore else */ if (pi === pl) {
// ran out of pattern, still have file left.
// this is only acceptable if we're on the very last
// empty segment of a file with a trailing slash.
// a/* should match a/b/
var emptyFileEnd = (fi === fl - 1) && (file[fi] === '')
return emptyFileEnd
return (fi === fl - 1) && (file[fi] === '')
}

// should be unreachable.
/* istanbul ignore next */
throw new Error('wtf?')
}

Expand Down

2 comments on commit a8763f4

@guitos
Copy link

@guitos guitos commented on a8763f4 Oct 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CVE-2022-3517 has been assigned for this issue.

@Mex505
Copy link

@Mex505 Mex505 commented on a8763f4 Oct 21, 2022 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.