nodejs22/0004-CVE-2026-13149-brace-expansion-unbound-recursion.patch
RHEL Packaging Agent 265841473c Fix CVE-2026-13149 (npm/brace-expansion)
Backport fix for CVE-2026-13149, an unbound recursion
vulnerability in the brace-expansion module vendored by npm.

The upstream TypeScript fix was manually adapted to the
JavaScript version (brace-expansion 2.0.2) shipped in
Node.js 22. Key changes: wrap expand() body in a for-loop
to avoid recursive stack exhaustion on non-expanding {}
groups, defer post-expansion until a brace set is confirmed
to expand (preventing O(2^n) blowup), and compute post
inline for the $ prefix case.

CVE: CVE-2026-13149
Upstream patches:
 - c7e33ec13a.patch
Resolves: RHEL-208653

This commit was backported by Ymir, a Red Hat Enterprise Linux software maintenance AI agent.

Assisted-by: Ymir
2026-08-03 16:10:42 +02:00

101 lines
3.4 KiB
Diff

From 213dc171de329a09f34c7a3222cad723ee65d693 Mon Sep 17 00:00:00 2001
From: RHEL Packaging Agent <redhat-ymir-agent@redhat.com>
Date: Tue, 14 Jul 2026 08:27:05 +0000
Subject: [PATCH] CVE-2026-13149: Fix unbound recursion in brace-expansion
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.
Adapted from upstream TypeScript fix to the JavaScript version (2.0.2)
vendored in Node.js.
Upstream: https://github.com/juliangruber/brace-expansion/commit/c7e33ec
---
.../npm/node_modules/brace-expansion/index.js | 49 ++++++++++++-------
1 file changed, 32 insertions(+), 17 deletions(-)
diff --git a/deps/npm/node_modules/brace-expansion/index.js b/deps/npm/node_modules/brace-expansion/index.js
index d084bac4..5d5f61ff 100644
--- a/deps/npm/node_modules/brace-expansion/index.js
+++ b/deps/npm/node_modules/brace-expansion/index.js
@@ -99,21 +99,27 @@ function gte(i, y) {
function expand(str, max, isTop) {
var expansions = [];
- var m = balanced('{', '}', str);
- if (!m) return [str];
-
- // no need to expand pre, since it is guaranteed to be free of brace-sets
- var pre = m.pre;
- var post = m.post.length
- ? expand(m.post, max, false)
- : [''];
-
- if (/\$$/.test(m.pre)) {
- for (var k = 0; k < post.length && k < max; k++) {
- var 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 (;;) {
+ var m = balanced('{', '}', str);
+ if (!m) return [str];
+
+ // no need to expand pre, since it is guaranteed to be free of brace-sets
+ var pre = m.pre;
+
+ if (/\$$/.test(m.pre)) {
+ var post = m.post.length
+ ? expand(m.post, max, false)
+ : [''];
+ for (var k = 0; k < post.length && k < max; k++) {
+ var expansion = pre+ '{' + m.body + '}' + post[k];
+ expansions.push(expansion);
+ }
+ return expansions;
}
- } else {
+
var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
var isSequence = isNumericSequence || isAlphaSequence;
@@ -122,11 +128,20 @@ 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.
+ var post = m.post.length
+ ? expand(m.post, max, false)
+ : [''];
+
var n;
if (isSequence) {
n = m.body.split(/\.\./);
@@ -200,8 +215,8 @@ function expand(str, max, isTop) {
expansions.push(expansion);
}
}
- }
- return expansions;
+ return expansions;
+ }
}