high · 7.5CVE-2026-69152Aug 3, 2026

CVE-2026-69152: brace-expansion DoS via unbounded intermediate arrays

Shubham Kandhare
Security Engagement Manager, SecureLayer7

A crafted ~25 KB input can crash a Node.js process with an uncatchable out-of-memory error, or a ~400 KB input can stall the event loop for over two minutes, because the maxLength guard added in…

Packagebrace-expansion
Ecosystemnpm
Affected< 1.1.18
Fixed in1.1.18
CVE-2026-69152: brace-expansion DoS via unbounded intermediate arrays

The problem

The maxLength fix from 1.1.17 (CVE-2026-14257) enforces its limit inside combine(), which merges final results. Two intermediate structures are built before combine() ever runs and were never bounded.

First, when expanding comma alternatives like {a,b,c,...}, each alternative gets its own independent maxLength allowance. Results are pushed into a shared values array with no cumulative cap, so 400 alternatives each consuming the full default allowance (4,000,000 chars) can grow values to 400x that limit before combine() sees it.

This exhausts the V8 heap with an uncatchable OOM.

Second, expandSequence() was bounded only by max (result count), never by maxLength. Wide padded sequences like {0...01..100000} generate every element at full pad width, burning CPU proportional to max * width. The event loop blocks for minutes while memory stays flat, so the earlier length check never fires.

Proof of concept

A working proof-of-concept for CVE-2026-69152 in brace-expansion, with the exact payload below.

javascript
// Memory exhaustion (~25 KB input, uncatchable OOM, tested against 1.1.17)
import { expand } from 'brace-expansion'

const part = '{' + '0'.repeat(50) + '1..100000}'
const input = '{' + Array(400).fill(part).join(',') + '}'

try {
  expand(input)
} catch (e) {
  // never reached - the process is already dead
}
// => FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

// Event-loop stall (~400 KB input, ~2 min CPU block, tested against 1.1.17)
expand('{' + '0'.repeat(400_000) + '1..100000}')

Both attacks bypass the 1.1.17 mitigation because maxLength was enforced per-alternative rather than cumulatively. In the comma path, each recursive expand_() call received a fresh full maxLength budget, so the values array could grow to alternatives * maxLength characters before combine() could truncate it.

In the sequence path, expandSequence() never consulted maxLength at all, only max (count). V8 represents padded strings as cons-strings, keeping memory flat while the real cost is CPU time, which is why this path survived the original audit.

The patch (commits 688a99ee and cb4b9e47 on the v1 branch) adds a running total of result count and character length inside the alternatives loop, stopping as soon as either bound is reached, and passes maxLength into expandSequence() so it stops generating once accumulated characters hit the limit.

CWE-770 (Allocation of Resources Without Limits or Throttling) applies to both paths.

The fix

Upgrade brace-expansion to **1.1.18** or later (v1 line). For v5 users, upgrade to **5.0.9** or later. There is no safe workaround on the affected versions: passing a small maxLength alone was not sufficient because the limit was applied per-alternative rather than cumulatively.

If an immediate upgrade is blocked, avoid passing any untrusted input to expand() or to glob/pattern libraries that call it transitively (minimatch, glob, fast-glob, etc.).

Reported by Alessio Della Libera (Numyra).

References: [1][2][3][4][5][6]

Related research