Denial of Service (resource exhaustion) via excessive glob expansion

HIGH
nodejs/node
Commit: aab12df670be
Affected: <=10.1.1
2026-04-05 11:33 UTC

Description

The commit patches a potential Denial of Service (resource exhaustion) vulnerability in minimatch by introducing a maximum expansions cap (EXPANSION_MAX) and allowing callers to supply a max option. Prior to this update (minimatch 10.1.1 and earlier), pathological glob patterns with brace expansions could trigger unbounded expansion, consuming excessive CPU and memory. The fix sets a default expansion cap of 1e5 and threads expansion through a max limit to prevent unbounded growth.

Proof of Concept

Node.js PoC to demonstrate DoS risk before fix by forcing large brace expansions in minimatch.expand. This uses two large brace expansions which would explosively generate combinations without a cap. In the vulnerable version, minimatch.expand(pattern) would generate a huge array; with the patch, it will cap at EXPANSION_MAX. Code (run in a controlled env): // PoC: triggers severe expansion in minimatch.expand (exploitable before fix) const minimatch = require('minimatch'); const M = 5000; // number of alternatives per brace; adjust to hit the expansion cap const items = Array.from({length: M}, (_, i) => 'p' + i); const pattern = '{' + items.join(',') + '}{' + items.join(',') + '}'; console.log('Expanding pattern with', M, 'per brace, total combos ~', M * M); try { const res = minimatch.expand(pattern); console.log('Expanded count:', res.length); } catch (err) { console.error('Error during expansion:', err && err.message ? err.message : err); }

Commit Details

Author: Node.js GitHub Bot

Date: 2026-02-08 11:25 UTC

Message:

deps: update minimatch to 10.1.2 PR-URL: https://github.com/nodejs/node/pull/61732 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Michaƫl Zasso <targos@protonmail.com>

Triage Assessment

Vulnerability Type: Denial of Service (resource exhaustion) // DoS via excessive glob expansion

Confidence: HIGH

Reasoning:

The commit updates minimatch to 10.1.2 and adds a maximum expansion limit (EXPANSION_MAX) with an option to cap recursive/glob expansions. This directly mitigates potential resource exhaustion/DoS via pathological glob patterns by preventing unbounded expansion.

Verification Assessment

Vulnerability Type: Denial of Service (resource exhaustion) via excessive glob expansion

Confidence: HIGH

Affected Versions: <=10.1.1

Code Diff

diff --git a/deps/minimatch/index.js b/deps/minimatch/index.js index 8491b26e1f40b6..519a68f788d64a 100644 --- a/deps/minimatch/index.js +++ b/deps/minimatch/index.js @@ -71,6 +71,7 @@ var require_commonjs2 = __commonJS({ "node_modules/@isaacs/brace-expansion/dist/commonjs/index.js"(exports2) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.EXPANSION_MAX = void 0; exports2.expand = expand; var balanced_match_1 = require_commonjs(); var escSlash = "\0SLASH" + Math.random() + "\0"; @@ -88,6 +89,7 @@ var require_commonjs2 = __commonJS({ var closePattern = /\\}/g; var commaPattern = /\\,/g; var periodPattern = /\\./g; + exports2.EXPANSION_MAX = 1e5; function numeric(str) { return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0); } @@ -118,14 +120,15 @@ var require_commonjs2 = __commonJS({ parts.push.apply(parts, p); return parts; } - function expand(str) { + function expand(str, options = {}) { if (!str) { return []; } + const { max = exports2.EXPANSION_MAX } = options; if (str.slice(0, 2) === "{}") { str = "\\{\\}" + str.slice(2); } - return expand_(escapeBraces(str), true).map(unescapeBraces); + return expand_(escapeBraces(str), max, true).map(unescapeBraces); } function embrace(str) { return "{" + str + "}"; @@ -139,15 +142,15 @@ var require_commonjs2 = __commonJS({ function gte(i, y) { return i >= y; } - function expand_(str, isTop) { + function expand_(str, max, isTop) { const expansions = []; const m = (0, balanced_match_1.balanced)("{", "}", str); if (!m) return [str]; const pre = m.pre; - const post = m.post.length ? expand_(m.post, false) : [""]; + const post = m.post.length ? expand_(m.post, max, false) : [""]; if (/\$$/.test(m.pre)) { - for (let k = 0; k < post.length; k++) { + for (let k = 0; k < post.length && k < max; k++) { const expansion = pre + "{" + m.body + "}" + post[k]; expansions.push(expansion); } @@ -159,7 +162,7 @@ var require_commonjs2 = __commonJS({ if (!isSequence && !isOptions) { if (m.post.match(/,(?!,).*\}/)) { str = m.pre + "{" + m.body + escClose + m.post; - return expand_(str); + return expand_(str, max, true); } return [str]; } @@ -169,7 +172,7 @@ var require_commonjs2 = __commonJS({ } else { n = parseCommaParts(m.body); if (n.length === 1 && n[0] !== void 0) { - n = expand_(n[0], false).map(embrace); + n = expand_(n[0], max, false).map(embrace); if (n.length === 1) { return post.map((p) => m.pre + n[0] + p); } @@ -215,11 +218,11 @@ var require_commonjs2 = __commonJS({ } else { N = []; for (let j = 0; j < n.length; j++) { - N.push.apply(N, expand_(n[j], false)); + N.push.apply(N, expand_(n[j], max, false)); } } for (let j = 0; j < N.length; j++) { - for (let k = 0; k < post.length; k++) { + for (let k = 0; k < post.length && expansions.length < max; k++) { const expansion = pre + N[j] + post[k]; if (!isTop || isSequence || expansion) { expansions.push(expansion); @@ -959,7 +962,7 @@ var path = { }; exports.sep = defaultPlatform === "win32" ? path.win32.sep : path.posix.sep; exports.minimatch.sep = exports.sep; -exports.GLOBSTAR = Symbol("globstar **"); +exports.GLOBSTAR = /* @__PURE__ */ Symbol("globstar **"); exports.minimatch.GLOBSTAR = exports.GLOBSTAR; var qmark = "[^/]"; var star = qmark + "*?"; diff --git a/deps/minimatch/package.json b/deps/minimatch/package.json index 03d3e966a5cb73..e303df7cc58550 100644 --- a/deps/minimatch/package.json +++ b/deps/minimatch/package.json @@ -2,7 +2,7 @@ "author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)", "name": "minimatch", "description": "a glob matcher in javascript", - "version": "10.1.1", + "version": "10.1.2", "repository": { "type": "git", "url": "git@github.com:isaacs/minimatch" @@ -39,23 +39,12 @@ "typedoc": "typedoc --tsconfig .tshy/esm.json ./src/*.ts", "node-build": "esbuild ./dist/commonjs/index.js --bundle --platform=node --outfile=index.js" }, - "prettier": { - "semi": false, - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "singleQuote": true, - "jsxSingleQuote": false, - "bracketSameLine": true, - "arrowParens": "avoid", - "endOfLine": "lf" - }, "engines": { "node": "20 || >=22" }, "devDependencies": { "@types/node": "^24.0.0", - "esbuild": "^0.27.0", + "esbuild": "^0.27.3", "mkdirp": "^3.0.1", "prettier": "^3.6.2", "tap": "^21.1.0", @@ -75,6 +64,6 @@ "type": "module", "module": "./dist/esm/index.js", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "@isaacs/brace-expansion": "^5.0.1" } }
← Back to Alerts View on GitHub →