From ec31284f9236eab90f087eb3d764c94402c7d1ee Mon Sep 17 00:00:00 2001 From: tjuhasz Date: Tue, 4 Aug 2026 17:00:29 +0200 Subject: [PATCH] Fix CVE-2026-69152 (brace-expansion) Update brace-expansion from 5.0.6 to 5.0.9 to address CVE-2026-13149 (memory exhaustion via deeply chained brace groups). Replaces 0001-CVE-2026-13149-Brace-Expansion-DOS.patch (5.0.6->5.0.7) with 0002-CVE-2026-69152-brace-expansion-5.0.9.patch (5.0.6->5.0.9) which includes the complete upstream fix. Resolves: RHEL-223774 --- 0001-CVE-2026-13149-Brace-Expansion-DOS.patch | 159 ----- ...CVE-2026-69152-brace-expansion-5.0.9.patch | 674 ++++++++++++++++++ nodejs24.spec | 6 +- 3 files changed, 676 insertions(+), 163 deletions(-) delete mode 100644 0001-CVE-2026-13149-Brace-Expansion-DOS.patch create mode 100644 0002-CVE-2026-69152-brace-expansion-5.0.9.patch diff --git a/0001-CVE-2026-13149-Brace-Expansion-DOS.patch b/0001-CVE-2026-13149-Brace-Expansion-DOS.patch deleted file mode 100644 index a711155..0000000 --- a/0001-CVE-2026-13149-Brace-Expansion-DOS.patch +++ /dev/null @@ -1,159 +0,0 @@ -From 193b73ce0138d3d2d62acd97fe2268bcdfcd8da6 Mon Sep 17 00:00:00 2001 -From: rpm-build -Date: Tue, 14 Jul 2026 09:01:08 +0000 -Subject: [PATCH] deps: update brace-expansion to 5.0.7 - -A run of non-expanding {} groups expanded post once per group before the -early returns that never use it, doubling the work on every group. expand_ -ran in O(2^n) and blocked for minutes on a ~90 byte input. - -Defer expanding post until a brace set is known to expand, and turn the -{a},b} restart into a loop so a long run of {} groups can't exhaust the -call stack. - -Backport of upstream fix: -https://github.com/juliangruber/brace-expansion/commit/c7e33ec13ac1a684c116720843ce24e208611754 - -CVE: CVE-2026-13149 ---- - .../brace-expansion/dist/commonjs/index.js | 38 ++++++++++++------- - .../brace-expansion/dist/esm/index.js | 38 ++++++++++++------- - .../node_modules/brace-expansion/package.json | 2 +- - 3 files changed, 49 insertions(+), 29 deletions(-) - -diff --git a/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js b/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js -index 33063dd..e4e5bd8 100644 ---- a/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js -+++ b/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js -@@ -95,19 +95,23 @@ function gte(i, y) { - function expand_(str, max, isTop) { - /** @type {string[]} */ - const expansions = []; -- const m = (0, balanced_match_1.balanced)('{', '}', str); -- if (!m) -- return [str]; -- // no need to expand pre, since it is guaranteed to be free of brace-sets -- const pre = m.pre; -- const post = m.post.length ? expand_(m.post, max, false) : ['']; -- if (/\$$/.test(m.pre)) { -- for (let k = 0; k < post.length && k < max; k++) { -- const expansion = pre + '{' + m.body + '}' + post[k]; -- expansions.push(expansion); -+ // 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. -+ for (;;) { -+ const m = (0, balanced_match_1.balanced)('{', '}', str); -+ if (!m) -+ return [str]; -+ // 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; - } -- } -- else { - const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); - const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); - const isSequence = isNumericSequence || isAlphaSequence; -@@ -116,10 +120,16 @@ function expand_(str, max, isTop) { - // {a},b} - if (m.post.match(/,(?!,).*\}/)) { - str = m.pre + '{' + m.body + escClose + m.post; -- return expand_(str, max, true); -+ isTop = true; -+ continue; - } - return [str]; - } -+ // 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; - if (isSequence) { - n = m.body.split(/\.\./); -@@ -195,7 +205,7 @@ function expand_(str, max, isTop) { - } - } - } -+ return expansions; - } -- return expansions; - } - //# sourceMappingURL=index.js.map -\ No newline at end of file -diff --git a/deps/npm/node_modules/brace-expansion/dist/esm/index.js b/deps/npm/node_modules/brace-expansion/dist/esm/index.js -index 32399e7..b2d2aa9 100644 ---- a/deps/npm/node_modules/brace-expansion/dist/esm/index.js -+++ b/deps/npm/node_modules/brace-expansion/dist/esm/index.js -@@ -91,19 +91,23 @@ function gte(i, y) { - function expand_(str, max, isTop) { - /** @type {string[]} */ - const expansions = []; -- const m = balanced('{', '}', str); -- if (!m) -- return [str]; -- // no need to expand pre, since it is guaranteed to be free of brace-sets -- const pre = m.pre; -- const post = m.post.length ? expand_(m.post, max, false) : ['']; -- if (/\$$/.test(m.pre)) { -- for (let k = 0; k < post.length && k < max; k++) { -- const expansion = pre + '{' + m.body + '}' + post[k]; -- expansions.push(expansion); -+ // 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. -+ for (;;) { -+ const m = balanced('{', '}', str); -+ if (!m) -+ return [str]; -+ // 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; - } -- } -- else { - const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); - const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); - const isSequence = isNumericSequence || isAlphaSequence; -@@ -112,10 +116,16 @@ function expand_(str, max, isTop) { - // {a},b} - if (m.post.match(/,(?!,).*\}/)) { - str = m.pre + '{' + m.body + escClose + m.post; -- return expand_(str, max, true); -+ isTop = true; -+ continue; - } - return [str]; - } -+ // 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; - if (isSequence) { - n = m.body.split(/\.\./); -@@ -191,7 +201,7 @@ function expand_(str, max, isTop) { - } - } - } -+ return expansions; - } -- return expansions; - } - //# sourceMappingURL=index.js.map -\ No newline at end of file diff --git a/0002-CVE-2026-69152-brace-expansion-5.0.9.patch b/0002-CVE-2026-69152-brace-expansion-5.0.9.patch new file mode 100644 index 0000000..ec7ca02 --- /dev/null +++ b/0002-CVE-2026-69152-brace-expansion-5.0.9.patch @@ -0,0 +1,674 @@ +From d8919f08bff03ff671a17f7b672edcba8a756c86 Mon Sep 17 00:00:00 2001 +From: tjuhasz +Date: Tue, 4 Aug 2026 16:36:52 +0200 +Subject: [PATCH] deps: update brace-expansion to 5.0.9 + +Update brace-expansion package in deps/npm/node_modules to version 5.0.9 +to address CVE-2026-13149. + +Key security fix: +- Adds EXPANSION_MAX_LENGTH to cap the total character count of the + accumulator during expansion, preventing memory exhaustion from + deeply chained brace groups (CVE-2026-14257) +- Refactors expand_ to iterate top-level brace groups instead of + recursing on m.post, preventing stack overflow on deeply chained + input +- Extracts combine() helper that enforces both max count and + maxLength bounds at the single point where output grows + +CVE: CVE-2026-13149 +--- + .../brace-expansion/dist/commonjs/index.js | 246 ++++++++++++------ + .../brace-expansion/dist/esm/index.js | 244 +++++++++++------ + .../node_modules/brace-expansion/package.json | 6 +- + 3 files changed, 336 insertions(+), 160 deletions(-) + +diff --git a/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js b/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js +index 33063dd..869a6be 100644 +--- a/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js ++++ b/deps/npm/node_modules/brace-expansion/dist/commonjs/index.js +@@ -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'; +@@ -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); + } +@@ -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, +@@ -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 + '}'; +@@ -92,22 +103,117 @@ function lte(i, y) { + function gte(i, y) { + return i >= y; + } +-function expand_(str, max, isTop) { +- /** @type {string[]} */ +- const expansions = []; +- const m = (0, balanced_match_1.balanced)('{', '}', str); +- if (!m) +- return [str]; +- // no need to expand pre, since it is guaranteed to be free of brace-sets +- const pre = m.pre; +- const post = m.post.length ? expand_(m.post, max, false) : ['']; +- if (/\$$/.test(m.pre)) { +- for (let k = 0; k < post.length && k < max; k++) { +- const expansion = pre + '{' + m.body + '}' + post[k]; +- expansions.push(expansion); ++// 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; + } +- else { ++ 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); ++ // 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(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); + const isSequence = isNumericSequence || isAlphaSequence; +@@ -116,86 +222,68 @@ function expand_(str, max, isTop) { + // {a},b} + if (m.post.match(/,(?!,).*\}/)) { + str = m.pre + '{' + m.body + escClose + m.post; +- return expand_(str, max, true); ++ 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); + } +- let n; ++ if (firstGroup) { ++ dropEmpties = isTop && !isSequence; ++ firstGroup = false; ++ } ++ 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; + } + } + } ++ acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length); ++ if (!m.post.length) ++ break; ++ str = m.post; + } +- return expansions; ++ return acc; + } + //# sourceMappingURL=index.js.map +\ No newline at end of file +diff --git a/deps/npm/node_modules/brace-expansion/dist/esm/index.js b/deps/npm/node_modules/brace-expansion/dist/esm/index.js +index 32399e7..fd68f57 100644 +--- a/deps/npm/node_modules/brace-expansion/dist/esm/index.js ++++ b/deps/npm/node_modules/brace-expansion/dist/esm/index.js +@@ -15,6 +15,17 @@ const closePattern = /\\}/g; + const commaPattern = /\\,/g; + const periodPattern = /\\\./g; + export const 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. ++export const EXPANSION_MAX_LENGTH = 4_000_000; + function numeric(str) { + return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0); + } +@@ -64,7 +75,7 @@ export function expand(str, options = {}) { + if (!str) { + return []; + } +- const { max = EXPANSION_MAX } = options; ++ const { max = EXPANSION_MAX, maxLength = 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, +@@ -74,7 +85,7 @@ export 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 + '}'; +@@ -88,22 +99,117 @@ function lte(i, y) { + function gte(i, y) { + return i >= y; + } +-function expand_(str, max, isTop) { +- /** @type {string[]} */ +- const expansions = []; +- const m = balanced('{', '}', str); +- if (!m) +- return [str]; +- // no need to expand pre, since it is guaranteed to be free of brace-sets +- const pre = m.pre; +- const post = m.post.length ? expand_(m.post, max, false) : ['']; +- if (/\$$/.test(m.pre)) { +- for (let k = 0; k < post.length && k < max; k++) { +- const expansion = pre + '{' + m.body + '}' + post[k]; +- expansions.push(expansion); ++// 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; + } +- else { ++ 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 = balanced('{', '}', 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(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); + const isSequence = isNumericSequence || isAlphaSequence; +@@ -112,86 +218,68 @@ function expand_(str, max, isTop) { + // {a},b} + if (m.post.match(/,(?!,).*\}/)) { + str = m.pre + '{' + m.body + escClose + m.post; +- return expand_(str, max, true); ++ 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); + } +- let n; ++ if (firstGroup) { ++ dropEmpties = isTop && !isSequence; ++ firstGroup = false; ++ } ++ 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; + } + } + } ++ acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length); ++ if (!m.post.length) ++ break; ++ str = m.post; + } +- return expansions; ++ return acc; + } + //# sourceMappingURL=index.js.map +\ No newline at end of file +diff --git a/deps/npm/node_modules/brace-expansion/package.json b/deps/npm/node_modules/brace-expansion/package.json +index 8152480..4376400 100644 +--- a/deps/npm/node_modules/brace-expansion/package.json ++++ b/deps/npm/node_modules/brace-expansion/package.json +@@ -1,7 +1,7 @@ + { + "name": "brace-expansion", + "description": "Brace expansion as known from sh/bash", +- "version": "5.0.6", ++ "version": "5.0.9", + "files": [ + "dist" + ], +@@ -46,7 +46,7 @@ + }, + "license": "MIT", + "engines": { +- "node": "18 || 20 || >=22" ++ "node": "20 || >=22" + }, + "tshy": { + "exports": { +@@ -59,6 +59,6 @@ + "module": "./dist/esm/index.js", + "repository": { + "type": "git", +- "url": "git+ssh://git@github.com/juliangruber/brace-expansion.git" ++ "url": "git+https://github.com/juliangruber/brace-expansion.git" + } + } +-- +2.54.0 + diff --git a/nodejs24.spec b/nodejs24.spec index b9476d8..ac29a86 100644 --- a/nodejs24.spec +++ b/nodejs24.spec @@ -155,10 +155,8 @@ Source101: nodejs.srpm.macros # Sourced from: # https://github.com/nodejs/node/commit/fd350185539242b7d383ebf38f6041f10b472b39 0001-CVE-2026-59873-CVE-2026-59874-upgrade-bundled-tar-to-7.5.19.patch -# Sourced from: -# https://github.com/juliangruber/brace-expansion/commit/c7e33ec13ac1a684c116720843ce24e208611754 -0001-CVE-2026-13149-Brace-Expansion-DOS.patch - +# Fix security vulnerability in braces-expansion deps +0002-CVE-2026-69152-brace-expansion-5.0.9.patch %description Node.js is a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications.