Changeset 33517cc for node_modules/brace-expansion/dist/esm/index.js
- Timestamp:
- 09/19/26 10:30:30 (10 days ago)
- Branches:
- finki-main, main
- Children:
- 06ebe74
- Parents:
- 62b2964
- File:
-
- 1 edited
-
node_modules/brace-expansion/dist/esm/index.js (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
-
node_modules/brace-expansion/dist/esm/index.js
r62b2964 r33517cc 14 14 const closePattern = /\\}/g; 15 15 const commaPattern = /\\,/g; 16 const periodPattern = /\\ ./g;16 const periodPattern = /\\\./g; 17 17 export const EXPANSION_MAX = 100_000; 18 // `EXPANSION_MAX` caps the *number* of expansions, but not their length. An 19 // input like `'{a,b}'.repeat(1500)` stays under that count - its output is 20 // truncated to 100k results - while making every result ~1500 characters 21 // long. The result set, and the intermediate arrays built while combining 22 // brace sets, then grow large enough to exhaust memory and crash the process 23 // (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of 24 // characters the accumulator may hold at any point, so memory stays flat no 25 // matter how many brace groups are chained. The limit sits well above any 26 // realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M 27 // characters) so legitimate input is unaffected. 28 export const EXPANSION_MAX_LENGTH = 4_000_000; 29 // `expand_` recurses once per level of brace *nesting* - both when expanding a 30 // set's comma members and when re-wrapping a set whose body is a single part. 31 // The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one 32 // level per chained group), which left nesting depth unbounded: about 3,100 33 // levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack 34 // and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser 35 // will follow nesting. It sits far above any realistic pattern and well below 36 // the depth at which the stack runs out. 37 export const EXPANSION_MAX_DEPTH = 1_000; 38 // Bash keeps a quirk where a brace group followed by a comma set still expands 39 // (`{a},b}`). The parser implements it by rewriting the string and restarting 40 // the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n` 41 // full passes over a string that itself grows by one `escClose` sentinel each 42 // time - quadratic in `n`, with a ~26x constant from the sentinel's length. 43 // 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27 44 // seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many 45 // times the scan may restart. Real `{a},b}` input needs a handful. 46 export const EXPANSION_MAX_REWRITES = 1_000; 18 47 function numeric(str) { 19 48 return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0); … … 35 64 .replace(escPeriodPattern, '.'); 36 65 } 66 // Like `target.push(...items)` but doesn't overflow the stack 67 function pushAll(target, items) { 68 for (let i = 0; i < items.length; i++) { 69 target.push(items[i]); 70 } 71 } 37 72 /** 38 73 * Basically just str.split(","), but handling cases … … 41 76 */ 42 77 function parseCommaParts(str) { 43 if (!str) {44 return [''];45 }46 78 const parts = []; 47 const m = balanced('{', '}', str); 48 if (!m) { 49 return str.split(','); 50 } 51 const { pre, body, post } = m; 52 const p = pre.split(','); 53 p[p.length - 1] += '{' + body + '}'; 54 const postParts = parseCommaParts(post); 55 if (post.length) { 56 ; 57 p[p.length - 1] += postParts.shift(); 58 p.push.apply(p, postParts); 59 } 60 parts.push.apply(parts, p); 61 return parts; 79 // Walk the brace groups iteratively. Recursing on `post` once per group let a 80 // chain of them exhaust the stack - the parsing-side counterpart to 81 // the `expand_` overflow fixed for CVE-2026-14257, and not something `max` or 82 // `maxLength` can bound, since it happens before expansion. 83 // 84 // The part the next chunk continues 85 let carry = ''; 86 for (;;) { 87 const m = balanced('{', '}', str); 88 if (!m) { 89 const tail = str.split(','); 90 tail[0] = carry + tail[0]; 91 pushAll(parts, tail); 92 return parts; 93 } 94 const { pre, body, post } = m; 95 const p = pre.split(','); 96 p[0] = carry + p[0]; 97 p[p.length - 1] += '{' + body + '}'; 98 if (!post.length) { 99 pushAll(parts, p); 100 return parts; 101 } 102 carry = p.pop(); 103 pushAll(parts, p); 104 str = post; 105 } 62 106 } 63 107 export function expand(str, options = {}) { … … 65 109 return []; 66 110 } 67 const { max = EXPANSION_MAX } = options;111 const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH, maxDepth = EXPANSION_MAX_DEPTH, maxRewrites = EXPANSION_MAX_REWRITES, } = options; 68 112 // I don't know why Bash 4.3 does this, but it does. 69 113 // Anything starting with {} will have the first two bytes preserved … … 75 119 str = '\\{\\}' + str.slice(2); 76 120 } 77 return expand_(escapeBraces(str), max, true).map(unescapeBraces);121 return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces); 78 122 } 79 123 function embrace(str) { … … 89 133 return i >= y; 90 134 } 91 function expand_(str, max, isTop) { 92 /** @type {string[]} */ 93 const expansions = []; 94 const m = balanced('{', '}', str); 95 if (!m) 135 // Build `{ acc[a] + pre + values[v] }` for every combination, capping the 136 // number of results at `max` and the total number of characters at `maxLength`. 137 // This is the one place output grows, so bounding it here keeps the single 138 // accumulator - and therefore memory - flat regardless of how many brace groups 139 // are combined (CVE-2026-14257). 140 function combine(acc, pre, values, max, maxLength, dropEmpties) { 141 const out = []; 142 let length = 0; 143 for (let a = 0; a < acc.length; a++) { 144 for (let v = 0; v < values.length; v++) { 145 if (out.length >= max) 146 return out; 147 const expansion = acc[a] + pre + values[v]; 148 // Bash drops empty results at the top level. Skip them before they count 149 // against `max`, so `max` bounds the number of *kept* results. 150 if (dropEmpties && !expansion) 151 continue; 152 if (length + expansion.length > maxLength) 153 return out; 154 out.push(expansion); 155 length += expansion.length; 156 } 157 } 158 return out; 159 } 160 // The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`) 161 // sequence body. 162 function expandSequence(body, isAlphaSequence, max, maxLength) { 163 const n = body.split(/\.\./); 164 const N = []; 165 // A sequence body always splits into two or three parts, but the compiler 166 // can't know that. 167 /* c8 ignore start */ 168 if (n[0] === undefined || n[1] === undefined) { 169 return N; 170 } 171 /* c8 ignore stop */ 172 const x = numeric(n[0]); 173 const y = numeric(n[1]); 174 const width = Math.max(n[0].length, n[1].length); 175 let incr = n.length === 3 && n[2] !== undefined ? 176 Math.max(Math.abs(numeric(n[2])), 1) 177 : 1; 178 let test = lte; 179 const reverse = y < x; 180 if (reverse) { 181 incr *= -1; 182 test = gte; 183 } 184 const pad = n.some(isPadded); 185 let length = 0; 186 for (let i = x; test(i, y) && N.length < max; i += incr) { 187 let c; 188 if (isAlphaSequence) { 189 c = String.fromCharCode(i); 190 if (c === '\\') { 191 c = ''; 192 } 193 } 194 else { 195 c = String(i); 196 if (pad) { 197 const need = width - c.length; 198 if (need > 0) { 199 const z = new Array(need + 1).join('0'); 200 if (i < 0) { 201 c = '-' + z + c.slice(1); 202 } 203 else { 204 c = z + c; 205 } 206 } 207 } 208 } 209 if (length + c.length > maxLength) 210 break; 211 N.push(c); 212 length += c.length; 213 } 214 return N; 215 } 216 function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) { 217 // Too deeply nested to keep following: treat the rest as literal, the same 218 // way a group that cannot expand is already handled. Truncating rather than 219 // throwing keeps `expand` total, matching `max` and `maxLength`. 220 if (depth > maxDepth) { 96 221 return [str]; 97 // no need to expand pre, since it is guaranteed to be free of brace-sets 98 const pre = m.pre; 99 const post = m.post.length ? expand_(m.post, max, false) : ['']; 100 if (/\$$/.test(m.pre)) { 101 for (let k = 0; k < post.length && k < max; k++) { 102 const expansion = pre + '{' + m.body + '}' + post[k]; 103 expansions.push(expansion); 104 } 105 } 106 else { 222 } 223 // Consume the string's top-level brace groups left to right, threading a 224 // running set of combined prefixes (`acc`). Expanding the tail iteratively - 225 // rather than recursing on `m.post` once per group - keeps the native stack 226 // depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no 227 // longer overflow the stack, and leaves a single accumulator whose size 228 // `maxLength` bounds directly (CVE-2026-14257). 229 let acc = ['']; 230 // Bash drops empty results, but only when the *first* top-level group is a 231 // comma set - a sequence like `{a..\}` may legitimately yield ''. The drop 232 // is on the final strings, so it is applied to whichever `combine` produces 233 // them (the one with no brace set left in the tail). 234 // How many times the `{a},b}` rewrite below has restarted the scan. Each pass 235 // re-reads the whole string, so leaving this unbounded is quadratic. 236 let rewrites = 0; 237 let dropEmpties = false; 238 let firstGroup = true; 239 for (;;) { 240 const m = balanced('{', '}', str); 241 // No brace set left: the rest of the string is literal. 242 if (!m) { 243 return combine(acc, str, [''], max, maxLength, dropEmpties); 244 } 245 // no need to expand pre, since it is guaranteed to be free of brace-sets 246 const pre = m.pre; 247 if (/\$$/.test(pre)) { 248 acc = combine(acc, pre + '{' + m.body + '}', [''], max, maxLength, dropEmpties && !m.post.length); 249 firstGroup = false; 250 if (!m.post.length) 251 break; 252 str = m.post; 253 continue; 254 } 107 255 const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); 108 256 const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); … … 111 259 if (!isSequence && !isOptions) { 112 260 // {a},b} 113 if (m.post.match(/,(?!,).*\}/)) { 261 if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) { 262 rewrites++; 114 263 str = m.pre + '{' + m.body + escClose + m.post; 115 return expand_(str, max, true); 116 } 117 return [str]; 118 } 119 let n; 264 isTop = true; 265 continue; 266 } 267 // Nothing here expands, so the whole remaining string is literal. 268 return combine(acc, pre + '{' + m.body + '}' + m.post, [''], max, maxLength, dropEmpties); 269 } 270 if (firstGroup) { 271 dropEmpties = isTop && !isSequence; 272 firstGroup = false; 273 } 274 let values; 120 275 if (isSequence) { 121 n = m.body.split(/\.\./);276 values = expandSequence(m.body, isAlphaSequence, max, maxLength); 122 277 } 123 278 else { 124 n = parseCommaParts(m.body);279 let n = parseCommaParts(m.body); 125 280 if (n.length === 1 && n[0] !== undefined) { 126 281 // x{{a,b}}y ==> x{a}y x{b}y 127 n = expand_(n[0], max, false).map(embrace);282 n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace); 128 283 //XXX is this necessary? Can't seem to hit it in tests. 129 284 /* c8 ignore start */ 130 285 if (n.length === 1) { 131 return post.map(p => m.pre + n[0] + p); 286 acc = combine(acc, pre + n[0], [''], max, maxLength, dropEmpties && !m.post.length); 287 if (!m.post.length) 288 break; 289 str = m.post; 290 continue; 132 291 } 133 292 /* c8 ignore stop */ 134 293 } 135 } 136 // at this point, n is the parts, and we know it's not a comma set 137 // with a single entry. 138 let N; 139 if (isSequence && n[0] !== undefined && n[1] !== undefined) { 140 const x = numeric(n[0]); 141 const y = numeric(n[1]); 142 const width = Math.max(n[0].length, n[1].length); 143 let incr = n.length === 3 && n[2] !== undefined ? Math.abs(numeric(n[2])) : 1; 144 let test = lte; 145 const reverse = y < x; 146 if (reverse) { 147 incr *= -1; 148 test = gte; 149 } 150 const pad = n.some(isPadded); 151 N = []; 152 for (let i = x; test(i, y); i += incr) { 153 let c; 154 if (isAlphaSequence) { 155 c = String.fromCharCode(i); 156 if (c === '\\') { 157 c = ''; 294 // Values that `combine` is going to drop as empty produce no result, so 295 // they must not count against `max` - otherwise `{a,,b}` with `max: 2` 296 // would stop at `['a', '']` and yield one result instead of two. Skipping 297 // them outright keeps `values` bounded while leaving `max` a bound on 298 // *kept* results. 299 let dropsEmpties = dropEmpties && !m.post.length && !pre; 300 for (let d = 0; dropsEmpties && d < acc.length; d++) { 301 if (acc[d]) { 302 dropsEmpties = false; 303 } 304 } 305 values = []; 306 let valuesLength = 0; 307 outer: for (let j = 0; j < n.length; j++) { 308 const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false); 309 for (let k = 0; k < expanded.length; k++) { 310 const v = expanded[k]; 311 if (dropsEmpties && !v) 312 continue; 313 if (values.length >= max || 314 valuesLength + v.length > maxLength) { 315 break outer; 158 316 } 317 values.push(v); 318 valuesLength += v.length; 159 319 } 160 else { 161 c = String(i); 162 if (pad) { 163 const need = width - c.length; 164 if (need > 0) { 165 const z = new Array(need + 1).join('0'); 166 if (i < 0) { 167 c = '-' + z + c.slice(1); 168 } 169 else { 170 c = z + c; 171 } 172 } 173 } 174 } 175 N.push(c); 176 } 177 } 178 else { 179 N = []; 180 for (let j = 0; j < n.length; j++) { 181 N.push.apply(N, expand_(n[j], max, false)); 182 } 183 } 184 for (let j = 0; j < N.length; j++) { 185 for (let k = 0; k < post.length && expansions.length < max; k++) { 186 const expansion = pre + N[j] + post[k]; 187 if (!isTop || isSequence || expansion) { 188 expansions.push(expansion); 189 } 190 } 191 } 192 } 193 return expansions; 320 } 321 } 322 acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length); 323 if (!m.post.length) 324 break; 325 str = m.post; 326 } 327 return acc; 194 328 } 195 329 //# sourceMappingURL=index.js.map
Note:
See TracChangeset
for help on using the changeset viewer.
