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.
| Package | Affected versions | Patched versions |
|---|---|---|
minimatchnpm | < 3.0.5 | 3.0.5 |
Affected products
11- minimatch/minimatchdescription
- ghsa-coords10 versionspkg:npm/minimatchpkg:rpm/almalinux/nodejspkg:rpm/almalinux/nodejs-develpkg:rpm/almalinux/nodejs-docspkg:rpm/almalinux/nodejs-full-i18npkg:rpm/almalinux/nodejs-libspkg:rpm/almalinux/nodejs-nodemonpkg:rpm/almalinux/nodejs-packagingpkg:rpm/almalinux/nodejs-packaging-bundlerpkg:rpm/almalinux/npm
< 3.0.5+ 9 more
- (no CPE)range: < 3.0.5
- (no CPE)range: < 1:18.12.1-1.module_el9.1.0+16+91bc168f
- (no CPE)range: < 1:18.12.1-1.module_el9.1.0+16+91bc168f
- (no CPE)range: < 1:18.12.1-1.module_el9.1.0+16+91bc168f
- (no CPE)range: < 1:18.12.1-1.module_el9.1.0+16+91bc168f
- (no CPE)range: < 1:16.18.1-3.el9_1
- (no CPE)range: < 2.0.20-1.module_el9.1.0+16+91bc168f
- (no CPE)range: < 2021.06-4.module_el9.1.0+13+d9a595ea
- (no CPE)range: < 2021.06-4.module_el9.1.0+13+d9a595ea
- (no CPE)range: < 1:8.19.2-1.18.12.1.1.module_el9.1.0+16+91bc168f
Patches
1a8763f4388e5Improve redos protection, add many tests
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 modifiedtest/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- github.com/advisories/GHSA-f8q6-p94x-37v3ghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/MTEUUTNIEBHGKUKKLNUZSV7IEP6IP3Q3/mitrevendor-advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/UM6XJ73Q3NAM5KSGCOKJ2ZIA6GUWUJLK/mitrevendor-advisory
- nvd.nist.gov/vuln/detail/CVE-2022-3517ghsaADVISORY
- github.com/grafana/grafana-image-renderer/issues/329ghsaWEB
- github.com/isaacs/minimatch/commit/a8763f4388e51956be62dc6025cec1126beeb5e6ghsaWEB
- github.com/nodejs/node/issues/42510ghsaWEB
- lists.debian.org/debian-lts-announce/2023/01/msg00011.htmlghsamailing-listWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MTEUUTNIEBHGKUKKLNUZSV7IEP6IP3Q3ghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/UM6XJ73Q3NAM5KSGCOKJ2ZIA6GUWUJLKghsaWEB
News mentions
0No linked articles in our index yet.