Skip to content

CVE-2026-93687: braces vulnerable to stack-exhaustion denial of service through deeply nested patterns - #48

Merged
SeriousJul merged 2 commits into
mainfrom
factory/57-cve-2026-93687-braces-vulnerable-to-stack-exhaustion-denial-of-service-through-deeply-nested-patterns
Oct 6, 2026
Merged

SeriousJul merged 2 commits into
mainfrom
factory/57-cve-2026-93687-braces-vulnerable-to-stack-exhaustion-denial-of-service-through-deeply-nested-patterns

Conversation

@SeriousJul

Copy link
Copy Markdown
Owner

https://github.com/SeriousJul/seriousjul.github.io/security/dependabot/57

Package: braces
Ecosystem: npm
Vulnerable range: <= 3.0.3
Severity: high
CVSS score: 7.5

braces through 3.0.3 contains a stack overflow vulnerability in the recursive AST walkers that lack depth guards. Attackers can supply deeply nested brace patterns under the character limit to exhaust the call stack and terminate the Node.js process with an uncaught RangeError.

The recursive AST walkers in braces lack depth guards. A pattern nested
deep enough while staying under the 10000-character limit exhausts the
call stack and kills the Node.js process with an uncaught RangeError.
Measured on Node 20, a 4999-deep parenthesised pattern (9999 characters)
crashes braces.compile and braces.stringify.

No patched version exists: 3.0.3 is the latest release and the last
version in the vulnerable range <= 3.0.3 (GHSA-vfj7-8cjw-p6xm, upstream
issue micromatch/braces#70 still open). The registry copy is therefore
replaced with a vendored copy of 3.0.3 plus depth guards (cap of 100
nesting levels, throw a catchable RangeError beyond it), wired in with a
direct file: dependency and a braces override so the transitive copies
used by chokidar and micromatch resolve to it.

tests/braces-security.test.mjs pins the lockfile replacement and the
bounded-depth behaviour; vendor/braces/PATCH-NOTES.md records what
changed relative to 3.0.3 and how to drop the copy once upstream
publishes a fix.
@SeriousJul

Copy link
Copy Markdown
Owner Author

No patched version exists: dependency replaced, not upgraded

CVE-2026-93687 (GHSA-vfj7-8cjw-p6xm) covers braces <= 3.0.3 with first_patched_version: null. Verified against the registry and upstream on 2026-10-06:

So there is nothing to bump to. Per the alert workflow, the dependency is replaced: a vendored copy of 3.0.3 plus depth guards lives at vendor/braces/ (version declared 3.0.4, outside the vulnerable range) and is wired in through the root manifest:

"dependencies": { "braces": "file:vendor/braces" },
"overrides":    { "braces": "$braces" }

The override is what reaches the two transitive consumers: chokidar (~3.0.2, webpack file watching) and micromatch (^3.0.3, glob matching). The lockfile now resolves node_modules/braces to the vendored copy; the registry entry is gone.

The vulnerability, reproduced

The recursive AST walkers (compile, expand, stringify) have no depth guard; the only input limit is 10000 characters, which still allows 4999 nesting levels. Measured on Node 20 (the CI runtime): braces.compile on a 4999-deep parenthesised pattern (9999 chars, under the limit) dies with an uncaught RangeError: Maximum call stack size exceeded. On Node 26 the default stack is deeper and the same pattern survives, so the crash is platform dependent until the library bounds the depth itself.

What the vendored copy changes (see vendor/braces/PATCH-NOTES.md)

  • MAX_DEPTH: 100 in lib/constants.js.
  • lib/parse.js counts total nesting (braces and parentheses) and throws a catchable RangeError past the cap, mirroring the existing maxLength guard. options.maxDepth can only lower the cap.
  • The three recursive walkers carry a depth argument and throw the same guard, covering the direct-AST entry points (braces.compile(ast), .expand(ast), .stringify(ast)) that bypass the parser.
  • 4000 randomized patterns (depth <= 4) across all four APIs produce byte-identical output to unpatched 3.0.3.

Verification

  • tests/braces-security.test.mjs (new, 12 tests): pins the lockfile replacement outside <= 3.0.3 and asserts the guard fires at depth 101 with a bounded, catchable error instead of stack exhaustion, including a no-catch child-process probe shaped like the real chokidar/micromatch call sites.
  • Full suite: 75/75 pass (Node 26 and Node 20). npm run typecheck passes. npm run build (the Docusaurus production build, which drives webpack/chokidar/micromatch through the vendored copy) succeeds. npm ci from the committed lockfile resolves braces 3.0.4.

When upstream publishes a fix

Delete vendor/braces/ and the overrides/dependencies.braces entries, run npm install to restore the registry copy, and drop the now-stale test expectations if the message changes.

@SeriousJul
SeriousJul marked this pull request as ready for review October 6, 2026 22:15

@SeriousJul SeriousJul left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

PR Review: CVE-2026-93687 braces stack-exhaustion fix

Verified on Node 26 (CI uses Node 20): npm ci, npm test (75/75 pass, including the 13 braces tests), and npm run build all succeed. The installed node_modules/braces is a link to vendor/braces, so chokidar and micromatch both resolve the patched copy.

Verification performed

  • Diffed vendor/braces against the real braces@3.0.3 tarball. The only changes are the depth guards, the MAX_DEPTH constant, and the version bump. The console.log in lib/compile.js is upstream 3.0.3 original code, not introduced here.

  • Confirmed the advisory is real and lists no fixed version, which justifies the vendored copy.

  • Ran the no-catch child-process probe (4999-deep parens, 9999 chars): the process dies with the catchable guard RangeError, not with Maximum call stack size exceeded.

  • Confirmed ordinary patterns produce byte-identical output to 3.0.3 behavior, and depth 100 is accepted while 101 is rejected.

  • Score: 92 / 100

  • Specification Check: Pass

    • Lockfile resolves braces to exactly one entry (vendor/braces, 3.0.4, outside the vulnerable range <= 3.0.3); no registry copy remains.
    • Both transitive consumers (chokidar ~3.0.2, micromatch ^3.0.3) are satisfied by 3.0.4, so a reinstall cannot roll the fix back.
    • All four recursive entry points are guarded: parse (nesting of braces and parens) and the compile, expand, stringify AST walkers (which bypass parse when given an AST directly).
    • Failure mode is now a catchable RangeError thrown at depth 101 on any Node version, independent of call stack headroom.
    • Regression test is wired into the existing npm test script and pins the behavior the vendored copy must keep.
  • Quality Check: Pass

    • The patch is minimal and surgical: guards only, mirroring the existing maxLength guard. options.maxDepth can be lowered but never raised above the hard cap.
    • PATCH-NOTES.md documents the reason, the exact delta, and the removal path when upstream ships a fix.
    • The test suite is strong: it checks lockfile uniqueness, manifest override, dependent range satisfaction, boundary depth, direct AST entry points, and reads the error message rather than only checking "it threw".
  • Required Changes: (none blocking; two minor notes)

    • PATCH-NOTES.md says the rest of package.json is "verbatim from the published 3.0.3 tarball", but the vendored copy also drops scripts, devDependencies, and verb fields. Harmless, arguably cleaner, but the note should say so.
    • Consider a short comment in vendor/braces/lib/compile.js (or in PATCH-NOTES.md) flagging the inherited upstream console.log('node.isClose', ...) line, so a future maintainer does not mistake it for local debug code or "fix" it silently.

@SeriousJul
SeriousJul merged commit 1a3f4ba into main Oct 6, 2026
4 of 5 checks passed
SeriousJul added a commit that referenced this pull request Oct 8, 2026
This branch carried the vendored braces 3.0.4 from pull request #48, which
merged the same fix to main. Pull request #59 then merged ADR-0001, which
forbids local patches (vendor/ folder, file: dependencies, patch packages)
and fails the build on them via tests/dependency-policy.test.mjs.

ADR-0001 records the reachability analysis for this advisory: patterns reach
braces only through committed configuration in micromatch and chokidar, and
no outside party can supply a pattern to this build, so the project does not
qualify. The approved response for this alert is a Dependabot dismissal with
reason not_used, a maintainer decision made after the fact is documented.

No patched version of braces exists: 3.0.3 is the latest registry release and
the last version in the vulnerable range <= 3.0.3, so there is no registry
upgrade to make. This revert restores the registry lockfile so this branch
matches the policy on main and carries no competing fix.
SeriousJul added a commit that referenced this pull request Oct 8, 2026
…ce through deeply nested patterns (#62)

* factory: hold the branch for the pull request

* fix(deps): replace braces 3.0.3 with vendored 3.0.4 (CVE-2026-93687)

The recursive AST walkers in braces lack depth guards. A pattern nested
deep enough while staying under the 10000-character limit exhausts the
call stack and kills the Node.js process with an uncaught RangeError.
Measured on Node 20, a 4999-deep parenthesised pattern (9999 characters)
crashes braces.compile and braces.stringify.

No patched version exists: 3.0.3 is the latest release and the last
version in the vulnerable range <= 3.0.3 (GHSA-vfj7-8cjw-p6xm, upstream
issue micromatch/braces#70 still open). The registry copy is therefore
replaced with a vendored copy of 3.0.3 plus depth guards (cap of 100
nesting levels, throw a catchable RangeError beyond it), wired in with a
direct file: dependency and a braces override so the transitive copies
used by chokidar and micromatch resolve to it.

tests/braces-security.test.mjs pins the lockfile replacement and the
bounded-depth behaviour; vendor/braces/PATCH-NOTES.md records what
changed relative to 3.0.3 and how to drop the copy once upstream
publishes a fix.

* revert(deps): drop local braces patch per ADR-0001 (CVE-2026-93687)

This branch carried the vendored braces 3.0.4 from pull request #48, which
merged the same fix to main. Pull request #59 then merged ADR-0001, which
forbids local patches (vendor/ folder, file: dependencies, patch packages)
and fails the build on them via tests/dependency-policy.test.mjs.

ADR-0001 records the reachability analysis for this advisory: patterns reach
braces only through committed configuration in micromatch and chokidar, and
no outside party can supply a pattern to this build, so the project does not
qualify. The approved response for this alert is a Dependabot dismissal with
reason not_used, a maintainer decision made after the fact is documented.

No patched version of braces exists: 3.0.3 is the latest registry release and
the last version in the vulnerable range <= 3.0.3, so there is no registry
upgrade to make. This revert restores the registry lockfile so this branch
matches the policy on main and carries no competing fix.
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.

1 participant