Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 162 additions & 84 deletions node_modules/brace-expansion/dist/commonjs/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EXPANSION_MAX = void 0;
exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
exports.expand = expand;
const balanced_match_1 = require("balanced-match");
const escSlash = '\0SLASH' + Math.random() + '\0';
Expand All @@ -19,6 +19,17 @@ const closePattern = /\\}/g;
const commaPattern = /\\,/g;
const periodPattern = /\\\./g;
exports.EXPANSION_MAX = 100_000;
// `EXPANSION_MAX` caps the *number* of expansions, but not their length. An
// input like `'{a,b}'.repeat(1500)` stays under that count - its output is
// truncated to 100k results - while making every result ~1500 characters
// long. The result set, and the intermediate arrays built while combining
// brace sets, then grow large enough to exhaust memory and crash the process
// (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of
// characters the accumulator may hold at any point, so memory stays flat no
// matter how many brace groups are chained. The limit sits well above any
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
// characters) so legitimate input is unaffected.
exports.EXPANSION_MAX_LENGTH = 4_000_000;
function numeric(str) {
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
}
Expand Down Expand Up @@ -68,7 +79,7 @@ function expand(str, options = {}) {
if (!str) {
return [];
}
const { max = exports.EXPANSION_MAX } = options;
const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH } = options;
// I don't know why Bash 4.3 does this, but it does.
// Anything starting with {} will have the first two bytes preserved
// but *only* at the top level, so {},a}b will not expand to anything,
Expand All @@ -78,7 +89,7 @@ function expand(str, options = {}) {
if (str.slice(0, 2) === '{}') {
str = '\\{\\}' + str.slice(2);
}
return expand_(escapeBraces(str), max, true).map(unescapeBraces);
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
}
function embrace(str) {
return '{' + str + '}';
Expand All @@ -92,25 +103,116 @@ function lte(i, y) {
function gte(i, y) {
return i >= y;
}
function expand_(str, max, isTop) {
/** @type {string[]} */
const expansions = [];
// The `{a},b}` rewrite below restarts expansion on a rewritten string with
// the same `max` and `isTop = true`. Loop instead of recursing so a long run
// of non-expanding `{}` groups can't exhaust the call stack.
// Build `{ acc[a] + pre + values[v] }` for every combination, capping the
// number of results at `max` and the total number of characters at `maxLength`.
// This is the one place output grows, so bounding it here keeps the single
// accumulator - and therefore memory - flat regardless of how many brace groups
// are combined (CVE-2026-14257).
function combine(acc, pre, values, max, maxLength, dropEmpties) {
const out = [];
let length = 0;
for (let a = 0; a < acc.length; a++) {
for (let v = 0; v < values.length; v++) {
if (out.length >= max)
return out;
const expansion = acc[a] + pre + values[v];
// Bash drops empty results at the top level. Skip them before they count
// against `max`, so `max` bounds the number of *kept* results.
if (dropEmpties && !expansion)
continue;
if (length + expansion.length > maxLength)
return out;
out.push(expansion);
length += expansion.length;
}
}
return out;
}
// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
// sequence body.
function expandSequence(body, isAlphaSequence, max, maxLength) {
const n = body.split(/\.\./);
const N = [];
// A sequence body always splits into two or three parts, but the compiler
// can't know that.
/* c8 ignore start */
if (n[0] === undefined || n[1] === undefined) {
return N;
}
/* c8 ignore stop */
const x = numeric(n[0]);
const y = numeric(n[1]);
const width = Math.max(n[0].length, n[1].length);
let incr = n.length === 3 && n[2] !== undefined ?
Math.max(Math.abs(numeric(n[2])), 1)
: 1;
let test = lte;
const reverse = y < x;
if (reverse) {
incr *= -1;
test = gte;
}
const pad = n.some(isPadded);
let length = 0;
for (let i = x; test(i, y) && N.length < max; i += incr) {
let c;
if (isAlphaSequence) {
c = String.fromCharCode(i);
if (c === '\\') {
c = '';
}
}
else {
c = String(i);
if (pad) {
const need = width - c.length;
if (need > 0) {
const z = new Array(need + 1).join('0');
if (i < 0) {
c = '-' + z + c.slice(1);
}
else {
c = z + c;
}
}
}
}
if (length + c.length > maxLength)
break;
N.push(c);
length += c.length;
}
return N;
}
function expand_(str, max, maxLength, isTop) {
// Consume the string's top-level brace groups left to right, threading a
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
// rather than recursing on `m.post` once per group - keeps the native stack
// depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no
// longer overflow the stack, and leaves a single accumulator whose size
// `maxLength` bounds directly (CVE-2026-14257).
let acc = [''];
// Bash drops empty results, but only when the *first* top-level group is a
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
// is on the final strings, so it is applied to whichever `combine` produces
// them (the one with no brace set left in the tail).
let dropEmpties = false;
let firstGroup = true;
for (;;) {
const m = (0, balanced_match_1.balanced)('{', '}', str);
if (!m)
return [str];
// No brace set left: the rest of the string is literal.
if (!m) {
return combine(acc, str, [''], max, maxLength, dropEmpties);
}
// no need to expand pre, since it is guaranteed to be free of brace-sets
const pre = m.pre;
if (/\$$/.test(m.pre)) {
const post = m.post.length ? expand_(m.post, max, false) : [''];
for (let k = 0; k < post.length && k < max; k++) {
const expansion = pre + '{' + m.body + '}' + post[k];
expansions.push(expansion);
}
return expansions;
if (/\$$/.test(pre)) {
acc = combine(acc, pre + '{' + m.body + '}', [''], max, maxLength, dropEmpties && !m.post.length);
firstGroup = false;
if (!m.post.length)
break;
str = m.post;
continue;
}
const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
Expand All @@ -123,89 +225,65 @@ function expand_(str, max, isTop) {
isTop = true;
continue;
}
return [str];
// Nothing here expands, so the whole remaining string is literal.
return combine(acc, pre + '{' + m.body + '}' + m.post, [''], max, maxLength, dropEmpties);
}
if (firstGroup) {
dropEmpties = isTop && !isSequence;
firstGroup = false;
}
// Only expand post once we know this brace set actually expands. Computing
// it before the early returns above expanded post a second time on every
// non-expanding `{}`, which is what made inputs like `a{},{},{}...` blow up
// exponentially.
const post = m.post.length ? expand_(m.post, max, false) : [''];
let n;
let values;
if (isSequence) {
n = m.body.split(/\.\./);
values = expandSequence(m.body, isAlphaSequence, max, maxLength);
}
else {
n = parseCommaParts(m.body);
let n = parseCommaParts(m.body);
if (n.length === 1 && n[0] !== undefined) {
// x{{a,b}}y ==> x{a}y x{b}y
n = expand_(n[0], max, false).map(embrace);
n = expand_(n[0], max, maxLength, false).map(embrace);
//XXX is this necessary? Can't seem to hit it in tests.
/* c8 ignore start */
if (n.length === 1) {
return post.map(p => m.pre + n[0] + p);
acc = combine(acc, pre + n[0], [''], max, maxLength, dropEmpties && !m.post.length);
if (!m.post.length)
break;
str = m.post;
continue;
}
/* c8 ignore stop */
}
}
// at this point, n is the parts, and we know it's not a comma set
// with a single entry.
let N;
if (isSequence && n[0] !== undefined && n[1] !== undefined) {
const x = numeric(n[0]);
const y = numeric(n[1]);
const width = Math.max(n[0].length, n[1].length);
let incr = n.length === 3 && n[2] !== undefined ?
Math.max(Math.abs(numeric(n[2])), 1)
: 1;
let test = lte;
const reverse = y < x;
if (reverse) {
incr *= -1;
test = gte;
}
const pad = n.some(isPadded);
N = [];
for (let i = x; test(i, y) && N.length < max; i += incr) {
let c;
if (isAlphaSequence) {
c = String.fromCharCode(i);
if (c === '\\') {
c = '';
}
// Values that `combine` is going to drop as empty produce no result, so
// they must not count against `max` - otherwise `{a,,b}` with `max: 2`
// would stop at `['a', '']` and yield one result instead of two. Skipping
// them outright keeps `values` bounded while leaving `max` a bound on
// *kept* results.
let dropsEmpties = dropEmpties && !m.post.length && !pre;
for (let d = 0; dropsEmpties && d < acc.length; d++) {
if (acc[d]) {
dropsEmpties = false;
}
else {
c = String(i);
if (pad) {
const need = width - c.length;
if (need > 0) {
const z = new Array(need + 1).join('0');
if (i < 0) {
c = '-' + z + c.slice(1);
}
else {
c = z + c;
}
}
}
}
N.push(c);
}
}
else {
N = [];
for (let j = 0; j < n.length; j++) {
N.push.apply(N, expand_(n[j], max, false));
}
}
for (let j = 0; j < N.length; j++) {
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);
values = [];
let valuesLength = 0;
outer: for (let j = 0; j < n.length; j++) {
const expanded = expand_(n[j], max, maxLength, false);
for (let k = 0; k < expanded.length; k++) {
const v = expanded[k];
if (dropsEmpties && !v)
continue;
if (values.length >= max || valuesLength + v.length > maxLength) {
break outer;
}
values.push(v);
valuesLength += v.length;
}
}
}
return expansions;
acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length);
if (!m.post.length)
break;
str = m.post;
}
return acc;
}
//# sourceMappingURL=index.js.map
Loading
Loading