public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: tjuhasz <tjuhasz@redhat.com>
To: git-commits@fedoraproject.org
Subject: [rpms/nodejs22] rawhide: Fix CVE-2027-13149 (npm/brace-expansion)
Date: Fri, 31 Jul 2026 08:03:35 GMT	[thread overview]
Message-ID: <178548501560.1.9493892779249822189.rpms-nodejs22-acecb42c35b5@fedoraproject.org> (raw)

            A new commit has been pushed.

            Repo   : rpms/nodejs22
            Branch : rawhide
            Commit : acecb42c35b58d4f60bc171a46bcedf55bdac1e7
            Author : tjuhasz <tjuhasz@redhat.com>
            Date   : 2026-07-30T15:46:49+02:00
            Stats  : +204/-0 in 3 file(s)
            URL    : https://src.fedoraproject.org/rpms/nodejs22/c/acecb42c35b58d4f60bc171a46bcedf55bdac1e7?branch=rawhide

            Log:
            Fix CVE-2027-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:
 - https://github.com/juliangruber/brace-expansion/commit/c7e33ec13ac1a684c116720843ce24e208611754.patch
Resolves: rhbz#2494970

---
diff --git a/0004-CVE-2026-25547-braces-expansion.patch b/0004-CVE-2026-25547-braces-expansion.patch
new file mode 100644
index 0000000..726ec71
--- /dev/null
+++ b/0004-CVE-2026-25547-braces-expansion.patch
@@ -0,0 +1,102 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: tjuhasz <tjuhasz@redhat.com>
+Date: Tue, 25 Feb 2026 14:21:26 +0100
+Subject: [PATCH] CVE-2026-25547: Fix brace expansion vulnerability
+
+Add expansion limit to prevent DoS attacks through excessive
+brace expansion in the brace-expansion module.
+
+---
+ deps/npm/node_modules/brace-expansion/index.js | 20 ++++++++++++--------
+ 1 file changed, 12 insertions(+), 8 deletions(-)
+
+diff --git a/deps/npm/node_modules/brace-expansion/index.js b/deps/npm/node_modules/brace-expansion/index.js
+--- a/deps/npm/node_modules/brace-expansion/index.js	2026-01-12 23:55:24.000000000 +0100
++++ b/deps/npm/node_modules/brace-expansion/index.js	2026-02-25 14:21:26.829483831 +0100
+@@ -8,6 +8,8 @@
+ var escComma = '\0COMMA'+Math.random()+'\0';
+ var escPeriod = '\0PERIOD'+Math.random()+'\0';
+ 
++const EXPANSION_MAX = 100_000;
++
+ function numeric(str) {
+   return parseInt(str, 10) == str
+     ? parseInt(str, 10)
+@@ -61,9 +63,11 @@
+   return parts;
+ }
+ 
+-function expandTop(str) {
++function expandTop(str, options = {}) {
+   if (!str)
+     return [];
++  
++  const { max = EXPANSION_MAX } = options;
+ 
+   // I don't know why Bash 4.3 does this, but it does.
+   // Anything starting with {} will have the first two bytes preserved
+@@ -75,7 +79,7 @@
+     str = '\\{\\}' + str.substr(2);
+   }
+ 
+-  return expand(escapeBraces(str), true).map(unescapeBraces);
++  return expand(escapeBraces(str), max, true).map(unescapeBraces);
+ }
+ 
+ function embrace(str) {
+@@ -92,7 +96,7 @@
+   return i >= y;
+ }
+ 
+-function expand(str, isTop) {
++function expand(str, max, isTop) {
+   var expansions = [];
+ 
+   var m = balanced('{', '}', str);
+@@ -101,11 +105,11 @@
+   // 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, false)
++    ? expand(m.post, max, false)
+     : [''];
+ 
+   if (/\$$/.test(m.pre)) {
+-    for (var k = 0; k < post.length; k++) {
++    for (var k = 0; k < post.length && k < max; k++) {
+       var expansion = pre+ '{' + m.body + '}' + post[k];
+       expansions.push(expansion);
+     }
+@@ -118,7 +122,7 @@
+       // {a},b}
+       if (m.post.match(/,(?!,).*\}/)) {
+         str = m.pre + '{' + m.body + escClose + m.post;
+-        return expand(str);
++        return expand(str, max, true);
+       }
+       return [str];
+     }
+@@ -130,7 +134,7 @@
+       n = parseCommaParts(m.body);
+       if (n.length === 1) {
+         // x{{a,b}}y ==> x{a}y x{b}y
+-        n = expand(n[0], false).map(embrace);
++        n = expand(n[0], max, false).map(embrace);
+         if (n.length === 1) {
+           return post.map(function(p) {
+             return m.pre + n[0] + p;
+@@ -185,12 +189,12 @@
+       N = [];
+ 
+       for (var j = 0; j < n.length; j++) {
+-        N.push.apply(N, expand(n[j], false));
++        N.push.apply(N, expand(n[j], max, false));
+       }
+     }
+ 
+     for (var j = 0; j < N.length; j++) {
+-      for (var k = 0; k < post.length; k++) {
++      for (var k = 0; k < post.length && expansions.length < max; k++) {
+         var expansion = pre + N[j] + post[k];
+         if (!isTop || isSequence || expansion)
+           expansions.push(expansion);

diff --git a/0005-CVE-2026-13149-brace-expansion-unbound-recursion.patch b/0005-CVE-2026-13149-brace-expansion-unbound-recursion.patch
new file mode 100644
index 0000000..80626ce
--- /dev/null
+++ b/0005-CVE-2026-13149-brace-expansion-unbound-recursion.patch
@@ -0,0 +1,100 @@
+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;
++  }
+ }
+ 

diff --git a/nodejs22.spec b/nodejs22.spec
index 3c8a221..0cfa5ee 100644
--- a/nodejs22.spec
+++ b/nodejs22.spec
@@ -169,6 +169,8 @@ Source101:      nodejs.srpm.macros
 0001-fips-disable-options.patch
 0002-CVE-2026-42338-ip-address-security-fix.patch
 0003-CVE-2026-59873-CVE-2026-59874-upgrade-bundled-tar-to-7.5.19.patch
+0004-CVE-2026-25547-braces-expansion.patch
+0005-CVE-2026-13149-brace-expansion-unbound-recursion.patch
 
 %description
 Node.js is a platform built on Chrome's JavaScript runtime

                 reply	other threads:[~2026-07-31  8:03 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=178548501560.1.9493892779249822189.rpms-nodejs22-acecb42c35b5@fedoraproject.org \
    --to=tjuhasz@redhat.com \
    --cc=git-commits@fedoraproject.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox