VYPR
High severityNVD Advisory· Published Oct 17, 2022· Updated May 13, 2025

CVE-2022-3517

CVE-2022-3517

Description

A vulnerability was found in the minimatch package. This flaw allows a Regular Expression Denial of Service (ReDoS) when calling the braceExpand function with specific arguments, resulting in a Denial of Service.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2022-3517 is a Regular Expression Denial of Service (ReDoS) vulnerability in the minimatch JavaScript library's braceExpand function, allowing attackers to cause a denial of service.

The vulnerability resides in the minimatch library, a glob matching utility used internally by npm [1]. The braceExpand function is vulnerable to ReDoS when processing crafted input containing brace patterns. The regex /\.*\}/ is susceptible to catastrophic backtracking, leading to excessive CPU consumption [4].

Exploitation does not require authentication if the library processes user-supplied glob patterns. An attacker can provide a specially crafted string to the braceExpand function, causing the regular expression engine to take exponential time to evaluate, resulting in a denial of service [2][4]. The minimatch documentation explicitly warns that using untrusted input to generate regex patterns can lead to ReDoS [1].

The impact is a denial of service condition, where the application becomes unresponsive due to high CPU usage. This can affect services that rely on minimatch for pattern matching, such as npm or Grafana Image Renderer [3][4]. The CVSS score is 7.5 (High) [4].

The vulnerability is fixed in minimatch version 3.0.5 [4]. The commit a8763f4 improves ReDoS protection by adding tests and hardening the regex [2]. Users should update to the latest version. No workaround is available other than avoiding untrusted input to braceExpand [1].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
minimatchnpm
< 3.0.53.0.5

Affected products

11

Patches

1
a8763f4388e5

Improve redos protection, add many tests

https://github.com/isaacs/minimatchisaacsFeb 6, 2022via ghsa
9 files changed · +8163 59
  • minimatch.js+74 45 modified
    @@ -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: ')+' },
    @@ -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 + '*?'
     
     // ** 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) {
    @@ -45,7 +45,7 @@ function charSet (s) {
     }
     
     // normalizes slashes.
    -var slashSplit = /\/+/
    +const slashSplit = /\/+/
     
     minimatch.filter = filter
     function filter (pattern, options) {
    @@ -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 = {}
     
    @@ -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()
    @@ -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
    @@ -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
     
    @@ -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 = []
    @@ -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()
    @@ -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
    @@ -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)
       })
    @@ -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) {
    @@ -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)
    @@ -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?')
     }
     
    
  • package.json+2 2 modified
    @@ -9,7 +9,7 @@
       },
       "main": "minimatch.js",
       "scripts": {
    -    "test": "tap test/*.js --cov",
    +    "test": "tap",
         "preversion": "npm test",
         "postversion": "npm publish",
         "postpublish": "git push origin --all; git push origin --tags"
    @@ -21,7 +21,7 @@
         "brace-expansion": "^1.1.7"
       },
       "devDependencies": {
    -    "tap": "^10.3.2"
    +    "tap": "^15.1.6"
       },
       "license": "ISC",
       "files": [
    
  • package-lock.json+7984 1 modified
  • test/basic.js+54 5 modified
    @@ -27,10 +27,16 @@ tap.test('basic tests', function (t) {
         // options.debug = true
         var m = new mm.Minimatch(pattern, options)
         var r = m.makeRe()
    +    var r2 = mm.makeRe(pattern, options)
    +    t.equal(String(r), String(r2), 'same results from both makeRe fns')
         var expectRe = regexps[re++]
    -    expectRe = '/' + expectRe.slice(1, -1).replace(new RegExp('([^\\\\])/', 'g'), '$1\\\/') + '/'
    -    tapOpts.re = String(r) || JSON.stringify(r)
    -    tapOpts.re = '/' + tapOpts.re.slice(1, -1).replace(new RegExp('([^\\\\])/', 'g'), '$1\\\/') + '/'
    +    if (expectRe !== false) {
    +      expectRe = '/' + expectRe.slice(1, -1).replace(new RegExp('([^\\\\])/', 'g'), '$1\\\/') + '/'
    +      tapOpts.re = String(r) || JSON.stringify(r)
    +      tapOpts.re = '/' + tapOpts.re.slice(1, -1).replace(new RegExp('([^\\\\])/', 'g'), '$1\\\/') + '/'
    +    } else {
    +      tapOpts.re = r
    +    }
         tapOpts.files = JSON.stringify(f)
         tapOpts.pattern = pattern
         tapOpts.set = m.set
    @@ -39,7 +45,7 @@ tap.test('basic tests', function (t) {
         var actual = mm.match(f, pattern, options)
         actual.sort(alpha)
     
    -    t.equivalent(
    +    t.same(
           actual, expect,
           JSON.stringify(pattern) + ' ' + JSON.stringify(expect),
           tapOpts
    @@ -56,10 +62,53 @@ tap.test('global leak test', function (t) {
       var globalAfter = Object.keys(global).filter(function (k) {
         return (k !== '__coverage__' && k !== '__core-js_shared__')
       })
    -  t.equivalent(globalAfter, globalBefore, 'no new globals, please')
    +  t.same(globalAfter, globalBefore, 'no new globals, please')
       t.end()
     })
     
    +tap.test('invalid patterns', t => {
    +  const toolong = 'x'.repeat(64 * 1024) + 'y'
    +  const expectTooLong = { message: 'pattern is too long' }
    +  t.throws(() => mm.braceExpand(toolong), expectTooLong)
    +  t.throws(() => new mm.Minimatch(toolong), expectTooLong)
    +  t.throws(() => mm('xy', toolong), expectTooLong)
    +  t.throws(() => mm.match(['xy'], toolong), expectTooLong)
    +
    +  const invalid = { message: 'invalid pattern' }
    +  const invalids = [
    +    null,
    +    1234,
    +    NaN,
    +    Infinity,
    +    undefined,
    +    {a: 1},
    +    true,
    +    false,
    +  ]
    +  for (const i of invalids) {
    +    t.throws(() => mm.braceExpand(i), invalid)
    +    t.throws(() => new mm.Minimatch(i), invalid)
    +    t.throws(() => mm('xy', i), invalid)
    +    t.throws(() => mm.match(['xy'], i), invalid)
    +  }
    +
    +  t.end()
    +})
    +
    +tap.test('ctor is generator', t => {
    +  const m = mm.Minimatch('asdf')
    +  t.type(m, mm.Minimatch)
    +  t.equal(m.pattern, 'asdf')
    +  t.end()
    +})
    +
    +tap.test('nocomment matches nothing', t => {
    +  t.equal(mm('#comment', '#comment', { nocomment: false }), false)
    +  t.equal(mm('#comment', '#comment', { nocomment: true }), true)
    +  t.end()
    +})
    +
    +
     function alpha (a, b) {
       return a > b ? 1 : -1
     }
    
  • test/brace-expand.js+1 1 modified
    @@ -66,7 +66,7 @@ tap.test('brace expansion', function (t) {
       patterns.forEach(function (tc) {
         var p = tc[0],
           expect = tc[1]
    -    t.equivalent(minimatch.braceExpand(p), expect, p)
    +    t.same(minimatch.braceExpand(p), expect, p)
       })
       t.end()
     })
    
  • test/defaults.js+33 2 modified
    @@ -36,7 +36,7 @@ tap.test('basic tests', function (t) {
         var actual = mm.match(f, pattern, options)
         actual.sort(alpha)
     
    -    t.equivalent(
    +    t.same(
           actual,
           expect,
           JSON.stringify(pattern) + ' ' + JSON.stringify(expect),
    @@ -52,7 +52,38 @@ tap.test('global leak test', function (t) {
       var globalAfter = Object.keys(global).filter(function (k) {
         return (k !== '__coverage__')
       })
    -  t.equivalent(globalAfter, globalBefore, 'no new globals, please')
    +  t.same(globalAfter, globalBefore, 'no new globals, please')
    +  t.end()
    +})
    +
    +tap.test('empty defaults obj returns original ctor', t => {
    +  for (const empty of [{}, undefined, null, false, 1234, 'xyz']) {
    +    const defmm = mm.defaults({})
    +    t.equal(defmm, mm)
    +    const Class = mm.Minimatch.defaults({})
    +    t.equal(Class, mm.Minimatch)
    +  }
    +  t.end()
    +})
    +
    +tap.test('call defaults mm function', t => {
    +  const defmm = mm.defaults({ nocomment: true })
    +  t.equal(mm('# nocomment', '# nocomment'), false)
    +  t.equal(defmm('# nocomment', '# nocomment'), true)
    +  t.equal(defmm('# nocomment', '# nocomment', { nocomment: false }), false)
    +  const undef = defmm.defaults({ nocomment: false })
    +  t.equal(undef('# nocomment', '# nocomment'), false)
    +  const unm = new undef.Minimatch('asdf')
    +  t.same(unm.options, { nocomment: false })
    +  const UndefClass = defmm.Minimatch.defaults({ nocomment: false })
    +  const unmm = new UndefClass('asfd')
    +  t.same(unmm.options, { nocomment: false })
    +
    +  const f = defmm.filter('#nc')
    +  t.same(['x','#nc', 'y'].filter(f), ['#nc'])
    +  t.same(defmm.match(['x','#nc', 'y'], '#nc'), ['#nc'])
    +  t.same(defmm.braceExpand('# {a,b}'), ['# a', '# b'])
    +  t.same(defmm.makeRe('# {a,b}'), /^(?:\#\ a|\#\ b)$/)
       t.end()
     })
     
    
  • test/no-path-module.js+3 0 added
    @@ -0,0 +1,3 @@
    +const t = require('tap')
    +const mm = t.mock('../', { path: null })
    +t.equal(mm.sep, '/')
    
  • test/patterns.js+7 3 modified
    @@ -259,7 +259,10 @@ module.exports = [
       'https://github.com/isaacs/minimatch/issues/59',
       ['[z-a]', []],
       ['a/[2015-03-10T00:23:08.647Z]/z', []],
    -  ['[a-0][a-\u0100]', []]
    +  ['[a-0][a-\u0100]', []],
    +
    +  'comments match nothing',
    +  ['# ignore this', []],
     ]
     
     module.exports.regexps = [
    @@ -327,7 +330,7 @@ module.exports.regexps = [
       '/^(?:(?=.)a[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/][^/][^/][^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?)$/',
       '/^(?:\\[\\])$/',
       '/^(?:\\[abc)$/',
    -  '/^(?:(?=.)XYZ)$/i',
    +  '/^(?:XYZ)$/i',
       '/^(?:(?=.)ab[^/]*?)$/i',
       '/^(?:(?!\\.)(?=.)[ia][^/][ck])$/i',
       '/^(?:\\/(?!\\.)(?=.)[^/]*?|(?!\\.)(?=.)[^/]*?)$/',
    @@ -358,7 +361,8 @@ module.exports.regexps = [
       '/^(?:(?:(?!(?:\\/|^)\\.).)*?\\/\\.x\\/(?:(?!(?:\\/|^)\\.).)*?)$/',
       '/^(?:\\[z\\-a\\])$/',
       '/^(?:a\\/\\[2015\\-03\\-10T00:23:08\\.647Z\\]\\/z)$/',
    -  '/^(?:(?=.)\\[a-0\\][a-Ā])$/'
    +  '/^(?:(?=.)\\[a-0\\][a-Ā])$/',
    +  false,
     ]
     
     Object.defineProperty(module.exports, 'files', {
    
  • test/win-path-sep.js+5 0 added
    @@ -0,0 +1,5 @@
    +const t = require('tap')
    +const mm = t.mock('../', { path: { sep: '\\' }})
    +
    +t.equal(mm('x\\y\\z', 'x/y/*/z'), false)
    +t.equal(mm('x\\y\\w\\z', 'x/y/*/z'), true)
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

10

News mentions

0

No linked articles in our index yet.