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