index.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. var balanced = require('balanced-match');
  2. module.exports = expandTop;
  3. var escSlash = '\0SLASH'+Math.random()+'\0';
  4. var escOpen = '\0OPEN'+Math.random()+'\0';
  5. var escClose = '\0CLOSE'+Math.random()+'\0';
  6. var escComma = '\0COMMA'+Math.random()+'\0';
  7. var escPeriod = '\0PERIOD'+Math.random()+'\0';
  8. var EXPANSION_MAX = 100000
  9. // `EXPANSION_MAX` caps the *number* of expansions, but not their length. An
  10. // input like `'{a,b}'.repeat(1500)` stays under that count - its output is
  11. // truncated to 100k results - while making every result ~1500 characters
  12. // long. The result set, and the intermediate arrays built while combining
  13. // brace sets, then grow large enough to exhaust memory and crash the process
  14. // (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of
  15. // characters the accumulator may hold at any point, so memory stays flat no
  16. // matter how many brace groups are chained. The limit sits well above any
  17. // realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
  18. // characters) so legitimate input is unaffected.
  19. var EXPANSION_MAX_LENGTH = 4000000
  20. function numeric(str) {
  21. return parseInt(str, 10) == str
  22. ? parseInt(str, 10)
  23. : str.charCodeAt(0);
  24. }
  25. function escapeBraces(str) {
  26. return str.split('\\\\').join(escSlash)
  27. .split('\\{').join(escOpen)
  28. .split('\\}').join(escClose)
  29. .split('\\,').join(escComma)
  30. .split('\\.').join(escPeriod);
  31. }
  32. function unescapeBraces(str) {
  33. return str.split(escSlash).join('\\')
  34. .split(escOpen).join('{')
  35. .split(escClose).join('}')
  36. .split(escComma).join(',')
  37. .split(escPeriod).join('.');
  38. }
  39. // Basically just str.split(","), but handling cases
  40. // where we have nested braced sections, which should be
  41. // treated as individual members, like {a,{b,c},d}
  42. function parseCommaParts(str) {
  43. if (!str)
  44. return [''];
  45. var parts = [];
  46. var m = balanced('{', '}', str);
  47. if (!m)
  48. return str.split(',');
  49. var pre = m.pre;
  50. var body = m.body;
  51. var post = m.post;
  52. var p = pre.split(',');
  53. p[p.length-1] += '{' + body + '}';
  54. var postParts = parseCommaParts(post);
  55. if (post.length) {
  56. p[p.length-1] += postParts.shift();
  57. p.push.apply(p, postParts);
  58. }
  59. parts.push.apply(parts, p);
  60. return parts;
  61. }
  62. function expandTop(str, options) {
  63. if (!str)
  64. return [];
  65. options = options || {};
  66. var max = options.max == null ? EXPANSION_MAX : options.max;
  67. var maxLength = options.maxLength == null ? EXPANSION_MAX_LENGTH : options.maxLength;
  68. // I don't know why Bash 4.3 does this, but it does.
  69. // Anything starting with {} will have the first two bytes preserved
  70. // but *only* at the top level, so {},a}b will not expand to anything,
  71. // but a{},b}c will be expanded to [a}c,abc].
  72. // One could argue that this is a bug in Bash, but since the goal of
  73. // this module is to match Bash's rules, we escape a leading {}
  74. if (str.substr(0, 2) === '{}') {
  75. str = '\\{\\}' + str.substr(2);
  76. }
  77. return expand(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
  78. }
  79. function embrace(str) {
  80. return '{' + str + '}';
  81. }
  82. function isPadded(el) {
  83. return /^-?0\d/.test(el);
  84. }
  85. function lte(i, y) {
  86. return i <= y;
  87. }
  88. function gte(i, y) {
  89. return i >= y;
  90. }
  91. // Build `{ acc[a] + pre + values[v] }` for every combination, capping the
  92. // number of results at `max` and the total number of characters at `maxLength`.
  93. // This is the one place output grows, so bounding it here keeps the single
  94. // accumulator - and therefore memory - flat regardless of how many brace groups
  95. // are combined (CVE-2026-14257).
  96. function combine(
  97. acc,
  98. pre,
  99. values,
  100. max,
  101. maxLength,
  102. dropEmpties
  103. ) {
  104. var out = []
  105. var length = 0
  106. for (var a = 0; a < acc.length; a++) {
  107. for (var v = 0; v < values.length; v++) {
  108. if (out.length >= max) return out
  109. var expansion = acc[a] + pre + values[v]
  110. // Bash drops empty results at the top level. Skip them before they count
  111. // against `max`, so `max` bounds the number of *kept* results.
  112. if (dropEmpties && !expansion) continue
  113. if (length + expansion.length > maxLength) return out
  114. out.push(expansion)
  115. length += expansion.length
  116. }
  117. }
  118. return out
  119. }
  120. // The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
  121. // sequence body.
  122. function expandSequence(
  123. body,
  124. isAlphaSequence,
  125. max,
  126. maxLength
  127. ) {
  128. var n = body.split(/\.\./)
  129. var N = []
  130. // A sequence body always splits into two or three parts, but the compiler
  131. // can't know that.
  132. /* c8 ignore start */
  133. if (n[0] === undefined || n[1] === undefined) {
  134. return N
  135. }
  136. /* c8 ignore stop */
  137. var x = numeric(n[0])
  138. var y = numeric(n[1])
  139. var width = Math.max(n[0].length, n[1].length)
  140. var incr =
  141. n.length === 3 && n[2] !== undefined ?
  142. Math.max(Math.abs(numeric(n[2])), 1)
  143. : 1
  144. var test = lte
  145. var reverse = y < x
  146. if (reverse) {
  147. incr *= -1
  148. test = gte
  149. }
  150. var pad = n.some(isPadded)
  151. var length = 0
  152. for (var i = x; test(i, y) && N.length < max; i += incr) {
  153. var c
  154. if (isAlphaSequence) {
  155. c = String.fromCharCode(i)
  156. if (c === '\\') {
  157. c = ''
  158. }
  159. } else {
  160. c = String(i)
  161. if (pad) {
  162. var need = width - c.length
  163. if (need > 0) {
  164. var z = new Array(need + 1).join('0')
  165. if (i < 0) {
  166. c = '-' + z + c.slice(1)
  167. } else {
  168. c = z + c
  169. }
  170. }
  171. }
  172. }
  173. if (length + c.length > maxLength) break
  174. N.push(c)
  175. length += c.length
  176. }
  177. return N
  178. }
  179. function expand(
  180. str,
  181. max,
  182. maxLength,
  183. isTop
  184. ) {
  185. // Consume the string's top-level brace groups left to right, threading a
  186. // running set of combined prefixes (`acc`). Expanding the tail iteratively -
  187. // rather than recursing on `m.post` once per group - keeps the native stack
  188. // depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no
  189. // longer overflow the stack, and leaves a single accumulator whose size
  190. // `maxLength` bounds directly (CVE-2026-14257).
  191. var acc = ['']
  192. // Bash drops empty results, but only when the *first* top-level group is a
  193. // comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
  194. // is on the final strings, so it is applied to whichever `combine` produces
  195. // them (the one with no brace set left in the tail).
  196. var dropEmpties = false
  197. var firstGroup = true
  198. for (;;) {
  199. const m = balanced('{', '}', str)
  200. // No brace set left: the rest of the string is literal.
  201. if (!m) {
  202. return combine(acc, str, [''], max, maxLength, dropEmpties)
  203. }
  204. // no need to expand pre, since it is guaranteed to be free of brace-sets
  205. const pre = m.pre
  206. if (/\$$/.test(pre)) {
  207. acc = combine(
  208. acc,
  209. pre + '{' + m.body + '}',
  210. [''],
  211. max,
  212. maxLength,
  213. dropEmpties && !m.post.length
  214. )
  215. firstGroup = false
  216. if (!m.post.length) break
  217. str = m.post
  218. continue
  219. }
  220. var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
  221. var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
  222. var isSequence = isNumericSequence || isAlphaSequence;
  223. var isOptions = m.body.indexOf(',') >= 0;
  224. if (!isSequence && !isOptions) {
  225. // {a},b}
  226. if (m.post.match(/,(?!,).*\}/)) {
  227. str = m.pre + '{' + m.body + escClose + m.post;
  228. isTop = true;
  229. continue;
  230. }
  231. // Nothing here expands, so the whole remaining string is literal.
  232. return combine(
  233. acc,
  234. pre + '{' + m.body + '}' + m.post,
  235. [''],
  236. max,
  237. maxLength,
  238. dropEmpties
  239. )
  240. }
  241. if (firstGroup) {
  242. dropEmpties = isTop && !isSequence
  243. firstGroup = false
  244. }
  245. var values;
  246. if (isSequence) {
  247. values = expandSequence(m.body, isAlphaSequence, max, maxLength);
  248. } else {
  249. var n = parseCommaParts(m.body);
  250. if (n.length === 1 && n[0] !== undefined) {
  251. // x{{a,b}}y ==> x{a}y x{b}y
  252. n = expand(n[0], max, maxLength, false).map(embrace);
  253. //XXX is this necessary? Can't seem to hit it in tests.
  254. /* c8 ignore start */
  255. if (n.length === 1) {
  256. acc = combine(
  257. acc,
  258. pre + n[0],
  259. [''],
  260. max,
  261. maxLength,
  262. dropEmpties && !m.post.length
  263. )
  264. if (!m.post.length) break
  265. str = m.post
  266. continue
  267. }
  268. /* c8 ignore stop */
  269. }
  270. // Values that `combine` is going to drop as empty produce no result, so
  271. // they must not count against `max` - otherwise `{a,,b}` with `max: 2`
  272. // would stop at `['a', '']` and yield one result instead of two. Skipping
  273. // them outright keeps `values` bounded while leaving `max` a bound on
  274. // *kept* results.
  275. var dropsEmpties = dropEmpties && !m.post.length && !pre
  276. for (var d = 0; dropsEmpties && d < acc.length; d++) {
  277. if (acc[d]) {
  278. dropsEmpties = false
  279. }
  280. }
  281. values = []
  282. var valuesLength = 0
  283. outer: for (var j = 0; j < n.length; j++) {
  284. var expanded = expand(n[j], max, maxLength, false)
  285. for (var k = 0; k < expanded.length; k++) {
  286. var v = expanded[k]
  287. if (dropsEmpties && !v) continue
  288. if (values.length >= max || valuesLength + v.length > maxLength) {
  289. break outer
  290. }
  291. values.push(v)
  292. valuesLength += v.length
  293. }
  294. }
  295. }
  296. acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length)
  297. if (!m.post.length) break
  298. str = m.post
  299. }
  300. return acc
  301. }