Skip to content

fix: backport GHSA-mh99-v99m-4gvg - #129

Merged
juliangruber merged 3 commits into
juliangruber:v1from
G-Rath:v1-backport
Jul 29, 2026
Merged

fix: backport GHSA-mh99-v99m-4gvg#129
juliangruber merged 3 commits into
juliangruber:v1from
G-Rath:v1-backport

Conversation

@G-Rath

@G-Rath G-Rath commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

@G-Rath

G-Rath commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

@juliangruber this looks to have landed cleanly, but I'll be doing a bit more cleanup and whatnot - I would appreciate knowing though that you're happy with landing this and if there's any particular changes you'd like to see for v1 etc specifically.

Assuming this is good after cleanup, I'll try to backport to v2 and v3 as well

@G-Rath

G-Rath commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

looks like @Trismaske had opened #132 to backport just the specific fix rather than the whole patch - I'm fine with either, having this backported is what I care about 🙂

that backport does not actually fix the issue, assuming something got left out that shouldn't have been

@juliangruber juliangruber left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same requests as in v2 PR

@sehmbimanvir

Copy link
Copy Markdown

When this v1 version will be released?

@juliangruber

Copy link
Copy Markdown
Owner

It needs to pass review first

@kdaisho

kdaisho commented Jul 27, 2026

Copy link
Copy Markdown

thank got. we need this fix!

@juliangruber juliangruber left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

Backports the CVE-2026-14257 fix from the v5 line onto v1. Three changes:

  1. expand becomes iterative - consumes top-level brace groups left to right threading an accumulator, instead of recursing on m.post once per group. Removes the stack overflow at ~2,700 chained groups.
  2. New maxLength option (default 4,000,000) enforced in a new combine helper, bounding total output characters.
  3. New test/cve.js with three regression tests.

The port is faithful to upstream src/index.ts, correctly de-TypeScripted, and kept ES5 (var throughout), which matters since v1 still declares an ie/8..latest testling matrix. All 193 existing tests pass, and I diffed base-vs-PR output across all 147 test/cases.txt entries: no regressions.

The approach is sound. A few things want addressing before merge.

Undocumented behavior change: ${...} no longer halts expansion

Base returned the whole string literally on /\$$/. The PR now continues expanding the tail. Seven inputs change behavior:

"${x}{a,b}"        base: ["${x}{a,b}"]         pr: ["${x}a","${x}b"]
"a${b}{c,d}e"      base: ["a${b}{c,d}e"]       pr: ["a${b}ce","a${b}de"]
"x${y}z{1..3}"     base: ["x${y}z{1..3}"]      pr: ["x${y}z1","x${y}z2","x${y}z3"]
"{a,b}${c}{d,e}"   base: ["a${c}{d,e}", ...]   pr: ["a${c}d","a${c}e","b${c}d","b${c}e"]

This is a fix, not a regression. I checked against bash (echo ${x}{a,b} gives a b) and against published 5.0.8, and the new behavior matches both. But it is a semantic change landing on a legacy line with very large downstream reach, and test/dollar.js misses it entirely: its three cases all use ${a,b} shapes where nothing expandable follows, so they pass either way.

Please add the covering case and call the change out in the PR body / release notes:

t.deepEqual(expand('${x}{a,b}'), ['${x}a', '${x}b']);

max default diverges from upstream, so the bound is characters-only

Upstream defaults max to EXPANSION_MAX = 100_000. Line 82 here keeps v1's Infinity. Because combine only tracks characters, zero-length expansions never advance the counter and out.length >= max never trips:

expand('{,}'.repeat(28))   // every expansion is '', so `length` stays 0

To be clear this is pre-existing and the PR improves it - on the base v1 the same input dies at 22 groups, on this branch it survives to 26. But the header comment claims memory "stays flat no matter how many brace groups are chained", and that is not true for this shape. Passing max explicitly closes it entirely ({max: 100000} handles 100 groups in ~57ms).

Keeping Infinity looks deliberate, since tests 188/191 assert "default is unbounded". If so, worth either softening that comment to say the v1 bound is characters-only, or adding a cheap out.length guard in combine that is independent of max. Flagging it as a decision rather than a defect.

Minor

  • concat-map is now an unused dependency. Line 1 still has var concatMap = require('concat-map'), but the PR removed its only call site (N = concatMap(n, ...)). Dropping the require and the dependencies entry is a free supply-chain reduction for the v1 line. (identity on line 98 is also dead, but that predates this PR.)
  • Comments reference EXPANSION_MAX, which does not exist on v1. Lines 12-20, copied verbatim from upstream. The rationale "100k results hitting EXPANSION_MAX measure ~1M characters" does not parse for a v1 reader, since there is no such constant on this branch. Needs rewording for the backport.
  • Typo from the mechanical const to var conversion. Line 212 reads "keeps the native stack depth varant"; upstream says "constant". Worth re-reading the other ported comments for the same collateral.
  • TypeScript and c8 leftovers in a plain-JS tape project. /* c8 ignore start|stop */ on lines 157, 161, 286, 300, plus the n[0] === undefined / n[2] !== undefined guards that exist only to satisfy tsc. v1 has no c8 and no compiler, so these are inert noise, and the if (n[0] === undefined || n[1] === undefined) early return is unreachable in JS.
  • Missing trailing newlines on both index.js and test/cve.js (\ No newline at end of file appears twice in the diff).
  • var fs = require('fs') in test/cve.js line 3 is unused.
  • test/cve.js runtime. The 50,000-group stack test plus the three {a,b} repeat loops add real wall time. Fine, just noting CI time grows.
  • t.doesNotThrow(() => { ... }) wrapping assertions means an inner assertion failure surfaces as a confusing throw rather than a clean tape failure.

Test coverage

Good regression coverage for the two vectors being fixed, and the "bound is a single accumulator, not groups * maxLength" loop is a nice touch. Gaps:

  • No test for the ${x}{a,b} behavior change above
  • No test pinning the zero-length-expansion case, if you decide to address it

Happy to re-review once these are in.

Comment thread index.js
This was referenced Aug 17, 2026
Jerome-Millot pushed a commit to leav-solutions/leav-engine that referenced this pull request Aug 26, 2026
version 1 has backport juliangruber/brace-expansion#129
version 2 has backport juliangruber/brace-expansion#130

but npm audit with GHSA-mh99-v99m-4gvg not take that into account yet !
staciamoon1-tech

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vulnerabilities in 1.1.16 version of this package