@@ -41344,7 +41344,7 @@ exports.range = range;
4134441344
4134541345
4134641346Object.defineProperty(exports, "__esModule", ({ value: true }));
41347- exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
41347+ exports.EXPANSION_MAX_REWRITES = exports.EXPANSION_MAX_DEPTH = exports. EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
4134841348exports.expand = expand;
4134941349const balanced_match_1 = __nccwpck_require__(2649);
4135041350const escSlash = '\0SLASH' + Math.random() + '\0';
@@ -41374,6 +41374,24 @@ exports.EXPANSION_MAX = 100_000;
4137441374// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
4137541375// characters) so legitimate input is unaffected.
4137641376exports.EXPANSION_MAX_LENGTH = 4_000_000;
41377+ // `expand_` recurses once per level of brace *nesting* - both when expanding a
41378+ // set's comma members and when re-wrapping a set whose body is a single part.
41379+ // The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
41380+ // level per chained group), which left nesting depth unbounded: about 3,100
41381+ // levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
41382+ // and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
41383+ // will follow nesting. It sits far above any realistic pattern and well below
41384+ // the depth at which the stack runs out.
41385+ exports.EXPANSION_MAX_DEPTH = 1_000;
41386+ // Bash keeps a quirk where a brace group followed by a comma set still expands
41387+ // (`{a},b}`). The parser implements it by rewriting the string and restarting
41388+ // the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
41389+ // full passes over a string that itself grows by one `escClose` sentinel each
41390+ // time - quadratic in `n`, with a ~26x constant from the sentinel's length.
41391+ // 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
41392+ // seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
41393+ // times the scan may restart. Real `{a},b}` input needs a handful.
41394+ exports.EXPANSION_MAX_REWRITES = 1_000;
4137741395function numeric(str) {
4137841396 return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
4137941397}
@@ -41393,37 +41411,52 @@ function unescapeBraces(str) {
4139341411 .replace(escCommaPattern, ',')
4139441412 .replace(escPeriodPattern, '.');
4139541413}
41414+ // Like `target.push(...items)` but doesn't overflow the stack
41415+ function pushAll(target, items) {
41416+ for (let i = 0; i < items.length; i++) {
41417+ target.push(items[i]);
41418+ }
41419+ }
4139641420/**
4139741421 * Basically just str.split(","), but handling cases
4139841422 * where we have nested braced sections, which should be
4139941423 * treated as individual members, like {a,{b,c},d}
4140041424 */
4140141425function parseCommaParts(str) {
41402- if (!str) {
41403- return [''];
41404- }
4140541426 const parts = [];
41406- const m = (0, balanced_match_1.balanced)('{', '}', str);
41407- if (!m) {
41408- return str.split(',');
41409- }
41410- const { pre, body, post } = m;
41411- const p = pre.split(',');
41412- p[p.length - 1] += '{' + body + '}';
41413- const postParts = parseCommaParts(post);
41414- if (post.length) {
41415- ;
41416- p[p.length - 1] += postParts.shift();
41417- p.push.apply(p, postParts);
41427+ // Walk the brace groups iteratively. Recursing on `post` once per group let a
41428+ // chain of them exhaust the stack - the parsing-side counterpart to
41429+ // the `expand_` overflow fixed for CVE-2026-14257, and not something `max` or
41430+ // `maxLength` can bound, since it happens before expansion.
41431+ //
41432+ // The part the next chunk continues
41433+ let carry = '';
41434+ for (;;) {
41435+ const m = (0, balanced_match_1.balanced)('{', '}', str);
41436+ if (!m) {
41437+ const tail = str.split(',');
41438+ tail[0] = carry + tail[0];
41439+ pushAll(parts, tail);
41440+ return parts;
41441+ }
41442+ const { pre, body, post } = m;
41443+ const p = pre.split(',');
41444+ p[0] = carry + p[0];
41445+ p[p.length - 1] += '{' + body + '}';
41446+ if (!post.length) {
41447+ pushAll(parts, p);
41448+ return parts;
41449+ }
41450+ carry = p.pop();
41451+ pushAll(parts, p);
41452+ str = post;
4141841453 }
41419- parts.push.apply(parts, p);
41420- return parts;
4142141454}
4142241455function expand(str, options = {}) {
4142341456 if (!str) {
4142441457 return [];
4142541458 }
41426- const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH } = options;
41459+ const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH, maxDepth = exports.EXPANSION_MAX_DEPTH, maxRewrites = exports.EXPANSION_MAX_REWRITES, } = options;
4142741460 // I don't know why Bash 4.3 does this, but it does.
4142841461 // Anything starting with {} will have the first two bytes preserved
4142941462 // but *only* at the top level, so {},a}b will not expand to anything,
@@ -41433,7 +41466,7 @@ function expand(str, options = {}) {
4143341466 if (str.slice(0, 2) === '{}') {
4143441467 str = '\\{\\}' + str.slice(2);
4143541468 }
41436- return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
41469+ return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
4143741470}
4143841471function embrace(str) {
4143941472 return '{' + str + '}';
@@ -41528,7 +41561,13 @@ function expandSequence(body, isAlphaSequence, max, maxLength) {
4152841561 }
4152941562 return N;
4153041563}
41531- function expand_(str, max, maxLength, isTop) {
41564+ function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
41565+ // Too deeply nested to keep following: treat the rest as literal, the same
41566+ // way a group that cannot expand is already handled. Truncating rather than
41567+ // throwing keeps `expand` total, matching `max` and `maxLength`.
41568+ if (depth > maxDepth) {
41569+ return [str];
41570+ }
4153241571 // Consume the string's top-level brace groups left to right, threading a
4153341572 // running set of combined prefixes (`acc`). Expanding the tail iteratively -
4153441573 // rather than recursing on `m.post` once per group - keeps the native stack
@@ -41540,6 +41579,9 @@ function expand_(str, max, maxLength, isTop) {
4154041579 // comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
4154141580 // is on the final strings, so it is applied to whichever `combine` produces
4154241581 // them (the one with no brace set left in the tail).
41582+ // How many times the `{a},b}` rewrite below has restarted the scan. Each pass
41583+ // re-reads the whole string, so leaving this unbounded is quadratic.
41584+ let rewrites = 0;
4154341585 let dropEmpties = false;
4154441586 let firstGroup = true;
4154541587 for (;;) {
@@ -41564,7 +41606,8 @@ function expand_(str, max, maxLength, isTop) {
4156441606 const isOptions = m.body.indexOf(',') >= 0;
4156541607 if (!isSequence && !isOptions) {
4156641608 // {a},b}
41567- if (m.post.match(/,(?!,).*\}/)) {
41609+ if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
41610+ rewrites++;
4156841611 str = m.pre + '{' + m.body + escClose + m.post;
4156941612 isTop = true;
4157041613 continue;
@@ -41584,7 +41627,7 @@ function expand_(str, max, maxLength, isTop) {
4158441627 let n = parseCommaParts(m.body);
4158541628 if (n.length === 1 && n[0] !== undefined) {
4158641629 // x{{a,b}}y ==> x{a}y x{b}y
41587- n = expand_(n[0], max, maxLength, false).map(embrace);
41630+ n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
4158841631 //XXX is this necessary? Can't seem to hit it in tests.
4158941632 /* c8 ignore start */
4159041633 if (n.length === 1) {
@@ -41610,12 +41653,13 @@ function expand_(str, max, maxLength, isTop) {
4161041653 values = [];
4161141654 let valuesLength = 0;
4161241655 outer: for (let j = 0; j < n.length; j++) {
41613- const expanded = expand_(n[j], max, maxLength, false);
41656+ const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
4161441657 for (let k = 0; k < expanded.length; k++) {
4161541658 const v = expanded[k];
4161641659 if (dropsEmpties && !v)
4161741660 continue;
41618- if (values.length >= max || valuesLength + v.length > maxLength) {
41661+ if (values.length >= max ||
41662+ valuesLength + v.length > maxLength) {
4161941663 break outer;
4162041664 }
4162141665 values.push(v);
0 commit comments