Skip to content

fix: prevent stack overflow from deeply nested patterns (CVE-2026-93687) - #72

Closed
FSDevelop wants to merge 8 commits into
micromatch:masterfrom
FSDevelop:fix/limit-nesting-depth
Closed

FSDevelop wants to merge 8 commits into
micromatch:masterfrom
FSDevelop:fix/limit-nesting-depth

Conversation

@FSDevelop

@FSDevelop FSDevelop commented Sep 21, 2026 •

Copy link
Copy Markdown

Summary

Fixes CVE-2026-93687 and closes #70.

Attacker-controlled, deeply nested brace patterns can exhaust the JavaScript call stack in recursive AST walkers while remaining within the existing input-length limit. This PR adds a bounded nesting limit to parsing and recursive AST processing.

Fix

  • Limit brace and parenthesis nesting to 100 levels while parsing.
  • Apply the same limit to compile, expand, and stringify so caller-supplied ASTs also receive depth checks.
  • Honor stricter maxDepth values, including fractional limits, while capping larger values at 100.
  • Preserve the previous stringify behavior with escapeInvalid: true.
  • Reject cyclic parent chains during AST expansion.
  • Document maxDepth and add regression coverage for excessive depth, fractional limits, stringify compatibility, and parent cycles.
  • Expose doesNotThrow in the Node 8.3 assertion shim, fixing the test compatibility issue introduced by the original depth-limit test.

The follow-up changes from FSDevelop#1, FSDevelop#2, and FSDevelop#3 are included.

Verification

At commit 28d440b5dd449dbf1fe6f3506cf94ecca4d02660:

  • 904 tests pass on actual Node 8.3.0 / Windows. Before the shim fix, the same runtime produced two assert.doesNotThrow is not a function failures; both now pass.
  • 904 tests pass on Node 26.1.0 / Windows, including a separate run forcing the assertion fallback.
  • Targeted checks reproduced the original fractional-limit, stringify, and cyclic-parent failures and verified their fixes. All four public methods accept nesting depth 100 and reject 101 for both braces and parentheses.
  • git diff --check passes.

These results cover the two Node versions and Windows environment listed above.

@FSDevelop FSDevelop changed the title fix: limit nesting depth to prevent stack overflow fix: prevent stack overflow from deeply nested patterns (CVE-2026-93687) Sep 21, 2026

@brendanh-katalyst brendanh-katalyst left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The changes look to be what is needed to solve the stack overflow issue behind vuln CVE-2026-93687

@xusheng-tw

Copy link
Copy Markdown

Any ETA on getting this merged and release?

@FSDevelop

Copy link
Copy Markdown
Author

Any ETA on getting this merged and release?

I sent an email to the developers that can approve, but no answer so far in the past 9 days.

@pday-kyc360

Copy link
Copy Markdown

+1 on this also, it's getting on to being 2 weeks+ on this

designsbymason added a commit to designsbymason/dbm-system that referenced this pull request Oct 3, 2026
…ide added

GHSA-vfj7-8cjw-p6xm / CVE-2026-93687 (braces, stack-overflow DoS in its
recursive AST walkers): no patched version exists upstream as of
2026-10-02. pnpm audit's own "Patched versions: >=3.0.4" claim is
stale/wrong — confirmed directly against the npm registry (3.0.3 is
genuinely dist-tags.latest) and GitHub's own Advisory API
(first_patched_version: null), and against the upstream repo (an open
fix PR, micromatch/braces#72, and open issues explicitly confirming no
fixed release exists).

No override added — there's no patched version to pin to, so one would
be a no-op, not a fix. Traced the actual code path instead:
@manypkg/get-packages (reached only via @changesets/cli, dev-only,
never shipped) feeds micromatch/braces only this repo's own
pnpm-workspace.yaml packages list — trusted, repo-owner-controlled
config, not external input an attacker could supply without already
having repo write access. Documented in §3.1 as the first real
instance of a new pattern (what to do when no patch exists at all),
and added to §3.2's scheduled-maintenance list to check again at the
next refresh pass. The Dependabot alert itself is left open, not
dismissed — that's a call for whoever owns the repo's security
settings, not something to do unilaterally.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@sol-aeternum

Copy link
Copy Markdown

Downstream follow-up: our application is blocked by GHSA-vfj7-8cjw-p6xm through Tailwind and Next ESLint dependency paths. We independently validated a minimal depth-guard backport against the published 3.0.3 release suite (all 764 tests pass unchanged on Node 24), plus excessive string/direct-AST depth, 100/101 boundaries, malformed/escaped inputs and glob/watch consumer checks. We retained original advisory visibility rather than treating package renaming as remediation. Is there a plan or ETA for reviewing this PR and publishing an official patched release? A release would let downstream consumers retire any temporary maintained patch without a broad toolchain migration. Thanks.

@Progrmaietor

Copy link
Copy Markdown

While validating this fix, I found a fractional maxDepth boundary case in the parser: parse('{{a,b},c}', { maxDepth: 1.5 }) accepts two nesting levels. The parser checks nesting >= maxDepth before incrementing, while the guarded walkers compare the resulting depth against the limit.

I prepared a focused correction and regression tests for both braces and parentheses. It passes the full existing suite (900 tests). The contribution targets this PR's source branch: FSDevelop#1

The change is small and keeps the parser's documented numeric limit consistent with the recursive walkers. Please incorporate it into this security fix if you agree.

@pillarsdotnet

Copy link
Copy Markdown

Thanks for this fix. I'm using it in a fork while it waits for review, and found one behaviour change in lib/stringify.js that isn't part of the depth limit.

The recursive call now passes the current node as parent:

output += stringify(child, node, child.nodes ? depth + 1 : depth);

Before, it passed no parent (stringify(child)), so parent defaulted to {}. With escapeInvalid: true, utils.isInvalidBrace(parent) now looks at a real parent, and valid nested braces come out escaped:

const braces = require('braces');
braces.stringify(braces.parse('{{a}}'), { escapeInvalid: true });
// 3.0.3:   '{{a}}'
// this PR: '\\{\\{a\\}\\}'

The same happens with '{a,{b}}', '{{x}y}', '{a,{b,{c}}' and '{}{a}'. Keeping the old parent while still threading the depth fixes it:

output += stringify(child, undefined, child.nodes ? depth + 1 : depth);

With that change, and a regression test for the five patterns above, the suite passes (900 tests) and the depth limit still stops the stack overflow. The commit is pillarsdotnet@31d783e, if you'd like to pick it up.

@dieub

dieub commented Oct 3, 2026

Copy link
Copy Markdown

I evaluated the depth guards from PR #72 at d0d575e55e74a4e0218e5248fafb79efc3e54ebb for a downstream [email protected] dependency path. Sharing a compatibility observation and bounded test results to help review/release planning.

The PR also changes the recursive stringify call from passing no parent to passing node. With escapeInvalid: true, this changes existing output independently of the depth guard:

const braces = require('braces');
console.log(braces.stringify('{1..8}', { escapeInvalid: true }));
// Published 3.0.3: {1..8}
// PR HEAD: \{1..8\}

For a narrow 3.0.3 backport experiment, I applied only this PR's five lib/ diffs to the published 3.0.3 source and retained the old parent handling while forwarding depth:

output += stringify(child, {}, child.nodes ? depth + 1 : depth);

This is a local derivative, not an assertion that the unmodified PR passes these compatibility checks. On Node 22.20.0 / linux-amd64, with network disabled during tests:

  • Released 3.0.3 test suite plus this PR's new security regression tests: 769 passing, zero failures (Mocha 11).
  • 12,019 adversarial/comparison assertions passed: 2,000 deterministic shallow/malformed patterns compared against published behavior for compile/expand/stringify with and without escapeInvalid; rejection at string nesting depths 101, 1,000 and 4,998; 10,000-level direct AST guards; configured-depth limits.
  • Representative micromatch and fast-glob normal patterns and guarded hostile patterns passed.
  • Published compile stack exhaustion was reproduced. An expand stack-overflow expectation at depth 4,000 was not reproduced with that fixture, so I do not claim that particular reproduction succeeded.

The full upstream HEAD suite separately passed 899 tests, but HEAD includes unreleased parser behavior; that result is distinct from the narrow backport results above. Expansion cardinality, AST width and malformed parent cycles remain outside this depth-only evaluation. No application-wide compatibility or production deployment is claimed.

Could the parent-propagation behavior change be separated or explicitly covered/documented, and could a fixed release be published after review? A local patch still leaves downstream version-based audit findings unresolved while the installed package is identified as affected 3.0.3. We are preserving the audit gate rather than suppressing it.

@Mluocheng

Copy link
Copy Markdown

预计何时能合并并发布? 着急 赶紧发布

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.

Security Advisory: Uncontrolled Recursion Depth in Tree Walkers Leads to Stack-Overflow DoS