snyk 1.1306.2 → 1.1306.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -86,6 +86,20 @@ var escClose = '\0CLOSE'+Math.random()+'\0';
86
86
  var escComma = '\0COMMA'+Math.random()+'\0';
87
87
  var escPeriod = '\0PERIOD'+Math.random()+'\0';
88
88
 
89
+ var EXPANSION_MAX = 100000
90
+
91
+ // `EXPANSION_MAX` caps the *number* of expansions, but not their length. An
92
+ // input like `'{a,b}'.repeat(1500)` stays under that count - its output is
93
+ // truncated to 100k results - while making every result ~1500 characters
94
+ // long. The result set, and the intermediate arrays built while combining
95
+ // brace sets, then grow large enough to exhaust memory and crash the process
96
+ // (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of
97
+ // characters the accumulator may hold at any point, so memory stays flat no
98
+ // matter how many brace groups are chained. The limit sits well above any
99
+ // realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
100
+ // characters) so legitimate input is unaffected.
101
+ var EXPANSION_MAX_LENGTH = 4000000
102
+
89
103
  function numeric(str) {
90
104
  return parseInt(str, 10) == str
91
105
  ? parseInt(str, 10)
@@ -144,7 +158,8 @@ function expandTop(str, options) {
144
158
  return [];
145
159
 
146
160
  options = options || {};
147
- var max = options.max == null ? Infinity : options.max;
161
+ var max = options.max == null ? EXPANSION_MAX : options.max;
162
+ var maxLength = options.maxLength == null ? EXPANSION_MAX_LENGTH : options.maxLength;
148
163
 
149
164
  // I don't know why Bash 4.3 does this, but it does.
150
165
  // Anything starting with {} will have the first two bytes preserved
@@ -156,7 +171,7 @@ function expandTop(str, options) {
156
171
  str = '\\{\\}' + str.substr(2);
157
172
  }
158
173
 
159
- return expand(escapeBraces(str), max, true).map(unescapeBraces);
174
+ return expand(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
160
175
  }
161
176
 
162
177
  function identity(e) {
@@ -177,15 +192,155 @@ function gte(i, y) {
177
192
  return i >= y;
178
193
  }
179
194
 
180
- function expand(str, max, isTop) {
181
- var expansions = [];
195
+ // Build `{ acc[a] + pre + values[v] }` for every combination, capping the
196
+ // number of results at `max` and the total number of characters at `maxLength`.
197
+ // This is the one place output grows, so bounding it here keeps the single
198
+ // accumulator - and therefore memory - flat regardless of how many brace groups
199
+ // are combined (CVE-2026-14257).
200
+ //
201
+ // `base[a]` is the length of the part of `acc[a]` that predates the current
202
+ // empty-drop baseline (see `expand`). The matching baselines for the results
203
+ // are appended to `outBase`, which the caller carries forward alongside them.
204
+ function combine(
205
+ acc,
206
+ base,
207
+ pre,
208
+ values,
209
+ max,
210
+ maxLength,
211
+ dropEmpties,
212
+ outBase
213
+ ) {
214
+ var out = []
215
+ var length = 0
216
+ for (var a = 0; a < acc.length; a++) {
217
+ for (var v = 0; v < values.length; v++) {
218
+ if (out.length >= max) return out
219
+ var expansion = acc[a] + pre + values[v]
220
+ // Bash drops empty results at the top level. Skip them before they count
221
+ // against `max`, so `max` bounds the number of *kept* results. "Empty"
222
+ // means "adds nothing past the baseline", not "empty overall".
223
+ if (dropEmpties && expansion.length === base[a]) continue
224
+ if (length + expansion.length > maxLength) return out
225
+ out.push(expansion)
226
+ outBase.push(base[a])
227
+ length += expansion.length
228
+ }
229
+ }
230
+ return out
231
+ }
232
+
233
+ // The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
234
+ // sequence body.
235
+ function expandSequence(
236
+ body,
237
+ isAlphaSequence,
238
+ max,
239
+ maxLength
240
+ ) {
241
+ var n = body.split(/\.\./)
242
+ var N = []
243
+ // A sequence body always splits into two or three parts, but the compiler
244
+ // can't know that.
245
+ /* c8 ignore start */
246
+ if (n[0] === undefined || n[1] === undefined) {
247
+ return N
248
+ }
249
+ /* c8 ignore stop */
250
+ var x = numeric(n[0])
251
+ var y = numeric(n[1])
252
+ var width = Math.max(n[0].length, n[1].length)
253
+ var incr =
254
+ n.length === 3 && n[2] !== undefined ?
255
+ Math.max(Math.abs(numeric(n[2])), 1)
256
+ : 1
257
+ var test = lte
258
+ var reverse = y < x
259
+ if (reverse) {
260
+ incr *= -1
261
+ test = gte
262
+ }
263
+ var pad = n.some(isPadded)
264
+
265
+ var length = 0
266
+ for (var i = x; test(i, y) && N.length < max; i += incr) {
267
+ var c
268
+ if (isAlphaSequence) {
269
+ c = String.fromCharCode(i)
270
+ if (c === '\\') {
271
+ c = ''
272
+ }
273
+ } else {
274
+ c = String(i)
275
+ if (pad) {
276
+ var need = width - c.length
277
+ if (need > 0) {
278
+ var z = new Array(need + 1).join('0')
279
+ if (i < 0) {
280
+ c = '-' + z + c.slice(1)
281
+ } else {
282
+ c = z + c
283
+ }
284
+ }
285
+ }
286
+ }
287
+ if (length + c.length > maxLength) break
288
+ N.push(c)
289
+ length += c.length
290
+ }
291
+ return N
292
+ }
293
+
294
+ function expand(
295
+ str,
296
+ max,
297
+ maxLength,
298
+ isTop
299
+ ) {
300
+ // Consume the string's top-level brace groups left to right, threading a
301
+ // running set of combined prefixes (`acc`). Expanding the tail iteratively -
302
+ // rather than recursing on `m.post` once per group - keeps the native stack
303
+ // depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no
304
+ // longer overflow the stack, and leaves a single accumulator whose size
305
+ // `maxLength` bounds directly (CVE-2026-14257).
306
+ var acc = ['']
307
+
308
+ // Bash drops empty results, but only when the *first* group of the run is a
309
+ // comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
310
+ // is on the final strings, so it is applied to whichever `combine` produces
311
+ // them (the one with no brace set left in the tail).
312
+ //
313
+ // The old implementation recursed on `m.post`, so the drop tested only the
314
+ // expansion of the current call's substring. The `{a},b}` rewrite below turns
315
+ // `isTop` back on part-way through a string, starting a fresh such run, so
316
+ // the drop must ignore whatever `acc` already holds from earlier groups.
317
+ // `accBase[a]` records how much of `acc[a]` predates the current run;
318
+ // `combine` treats an expansion as empty when it adds nothing past that.
319
+ var accBase = [0]
320
+ var dropEmpties = false
321
+ var firstGroup = true
322
+ var nextBase
182
323
 
183
- // The `{a},b}` rewrite below restarts expansion on a rewritten string with
184
- // the same `max` and `isTop = true`. Loop instead of recursing so a long run
185
- // of non-expanding `{}` groups can't exhaust the call stack.
186
324
  for (;;) {
187
325
  var m = balanced('{', '}', str);
188
- if (!m || /\$$/.test(m.pre)) return [str];
326
+
327
+ // No brace set left: the rest of the string is literal.
328
+ if (!m) {
329
+ return combine(acc, accBase, str, [''], max, maxLength, dropEmpties, [])
330
+ }
331
+
332
+ // no need to expand pre, since it is guaranteed to be free of brace-sets
333
+ var pre = m.pre;
334
+
335
+ // For compatibility reasons, `${` is not eligible for brace expansion, and
336
+ // on the 1.x line it suppresses expansion of the rest of the string too:
337
+ // the whole remainder is literal. The 2.x and 5.x lines instead keep
338
+ // expanding the tail, which is what bash does, but changing that here would
339
+ // be a breaking change for 1.x consumers. Routed through `combine` so the
340
+ // result is still bounded by `max` and `maxLength`.
341
+ if (/\$$/.test(pre)) {
342
+ return combine(acc, accBase, str, [''], max, maxLength, dropEmpties, [])
343
+ }
189
344
 
190
345
  var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
191
346
  var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
@@ -195,94 +350,112 @@ function expand(str, max, isTop) {
195
350
  // {a},b}
196
351
  if (m.post.match(/,(?!,).*\}/)) {
197
352
  str = m.pre + '{' + m.body + escClose + m.post;
353
+ // The rewritten string is expanded as if it were a fresh top-level one,
354
+ // so start a new empty-drop run: anchor the baseline at what `acc`
355
+ // holds now, and let the next expanding group decide whether to drop.
198
356
  isTop = true
357
+ firstGroup = true
358
+ dropEmpties = false
359
+ accBase = []
360
+ for (var b = 0; b < acc.length; b++) {
361
+ accBase.push(acc[b].length)
362
+ }
199
363
  continue
200
364
  }
201
- return [str];
365
+ // Nothing here expands, so the whole remaining string is literal.
366
+ return combine(
367
+ acc,
368
+ accBase,
369
+ pre + '{' + m.body + '}' + m.post,
370
+ [''],
371
+ max,
372
+ maxLength,
373
+ dropEmpties,
374
+ []
375
+ )
376
+ }
377
+
378
+ if (firstGroup) {
379
+ dropEmpties = isTop && !isSequence
380
+ firstGroup = false
202
381
  }
203
382
 
204
- var n;
383
+ var values;
205
384
  if (isSequence) {
206
- n = m.body.split(/\.\./);
385
+ values = expandSequence(m.body, isAlphaSequence, max, maxLength);
207
386
  } else {
208
- n = parseCommaParts(m.body);
209
- if (n.length === 1) {
387
+ var n = parseCommaParts(m.body);
388
+ if (n.length === 1 && n[0] !== undefined) {
210
389
  // x{{a,b}}y ==> x{a}y x{b}y
211
- n = expand(n[0], max, false).map(embrace);
390
+ n = expand(n[0], max, maxLength, false).map(embrace);
391
+ //XXX is this necessary? Can't seem to hit it in tests.
392
+ /* c8 ignore start */
212
393
  if (n.length === 1) {
213
- var post = m.post.length
214
- ? expand(m.post, max, false)
215
- : [''];
216
- return post.map(function(p) {
217
- return m.pre + n[0] + p;
218
- });
394
+ nextBase = []
395
+ acc = combine(
396
+ acc,
397
+ accBase,
398
+ pre + n[0],
399
+ [''],
400
+ max,
401
+ maxLength,
402
+ dropEmpties && !m.post.length,
403
+ nextBase
404
+ )
405
+ accBase = nextBase
406
+ if (!m.post.length) break
407
+ str = m.post
408
+ continue
409
+ }
410
+ /* c8 ignore stop */
411
+ }
412
+
413
+ // Values that `combine` is going to drop as empty produce no result, so
414
+ // they must not count against `max` - otherwise `{a,,b}` with `max: 2`
415
+ // would stop at `['a', '']` and yield one result instead of two. Skipping
416
+ // them outright keeps `values` bounded while leaving `max` a bound on
417
+ // *kept* results. A value is dropped when it adds nothing past the
418
+ // baseline, which is what `combine` tests.
419
+ var dropsEmpties = dropEmpties && !m.post.length && !pre
420
+ for (var d = 0; dropsEmpties && d < acc.length; d++) {
421
+ if (acc[d].length !== accBase[d]) {
422
+ dropsEmpties = false
219
423
  }
220
424
  }
221
- }
222
-
223
- // at this point, n is the parts, and we know it's not a comma set
224
- // with a single entry.
225
-
226
- // no need to expand pre, since it is guaranteed to be free of brace-sets
227
- var pre = m.pre;
228
- var post = m.post.length
229
- ? expand(m.post, max, false)
230
- : [''];
231
425
 
232
- var N;
233
-
234
- if (isSequence) {
235
- var x = numeric(n[0]);
236
- var y = numeric(n[1]);
237
- var width = Math.max(n[0].length, n[1].length)
238
- var incr = n.length == 3
239
- ? Math.max(Math.abs(numeric(n[2])), 1)
240
- : 1;
241
- var test = lte;
242
- var reverse = y < x;
243
- if (reverse) {
244
- incr *= -1;
245
- test = gte;
246
- }
247
- var pad = n.some(isPadded);
248
-
249
- N = [];
250
-
251
- for (var i = x; test(i, y) && N.length < max; i += incr) {
252
- var c;
253
- if (isAlphaSequence) {
254
- c = String.fromCharCode(i);
255
- if (c === '\\')
256
- c = '';
257
- } else {
258
- c = String(i);
259
- if (pad) {
260
- var need = width - c.length;
261
- if (need > 0) {
262
- var z = new Array(need + 1).join('0');
263
- if (i < 0)
264
- c = '-' + z + c.slice(1);
265
- else
266
- c = z + c;
267
- }
426
+ values = []
427
+ var valuesLength = 0
428
+ outer: for (var j = 0; j < n.length; j++) {
429
+ var expanded = expand(n[j], max, maxLength, false)
430
+ for (var k = 0; k < expanded.length; k++) {
431
+ var v = expanded[k]
432
+ if (dropsEmpties && !v) continue
433
+ if (values.length >= max || valuesLength + v.length > maxLength) {
434
+ break outer
268
435
  }
436
+ values.push(v)
437
+ valuesLength += v.length
269
438
  }
270
- N.push(c);
271
439
  }
272
- } else {
273
- N = concatMap(n, function(el) { return expand(el, max, false) });
274
440
  }
275
441
 
276
- for (var j = 0; j < N.length; j++) {
277
- for (var k = 0; k < post.length && expansions.length < max; k++) {
278
- var expansion = pre + N[j] + post[k];
279
- if (!isTop || isSequence || expansion)
280
- expansions.push(expansion);
281
- }
282
- }
283
-
284
- return expansions;
442
+ nextBase = []
443
+ acc = combine(
444
+ acc,
445
+ accBase,
446
+ pre,
447
+ values,
448
+ max,
449
+ maxLength,
450
+ dropEmpties && !m.post.length,
451
+ nextBase
452
+ )
453
+ accBase = nextBase
454
+ if (!m.post.length) break
455
+ str = m.post
285
456
  }
457
+
458
+ return acc
286
459
  }
287
460
 
288
461