Repository navigation
CVE-2026-93687: braces vulnerable to stack-exhaustion denial of service through deeply nested patterns - #48
Conversation
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.
No patched version exists: dependency replaced, not upgradedCVE-2026-93687 (GHSA-vfj7-8cjw-p6xm) covers
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 "dependencies": { "braces": "file:vendor/braces" },
"overrides": { "braces": "$braces" }The override is what reaches the two transitive consumers: The vulnerability, reproducedThe recursive AST walkers ( What the vendored copy changes (see
|
SeriousJul
left a comment
There was a problem hiding this comment.
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/bracesagainst the realbraces@3.0.3tarball. The only changes are the depth guards, theMAX_DEPTHconstant, and the version bump. Theconsole.loginlib/compile.jsis 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 withMaximum 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 thecompile,expand,stringifyAST walkers (which bypassparsewhen given an AST directly). - Failure mode is now a catchable
RangeErrorthrown at depth 101 on any Node version, independent of call stack headroom. - Regression test is wired into the existing
npm testscript and pins the behavior the vendored copy must keep.
- Lockfile resolves braces to exactly one entry (
-
Quality Check: Pass
- The patch is minimal and surgical: guards only, mirroring the existing
maxLengthguard.options.maxDepthcan be lowered but never raised above the hard cap. PATCH-NOTES.mddocuments 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".
- The patch is minimal and surgical: guards only, mirroring the existing
-
Required Changes: (none blocking; two minor notes)
PATCH-NOTES.mdsays the rest ofpackage.jsonis "verbatim from the published 3.0.3 tarball", but the vendored copy also dropsscripts,devDependencies, andverbfields. Harmless, arguably cleaner, but the note should say so.- Consider a short comment in
vendor/braces/lib/compile.js(or inPATCH-NOTES.md) flagging the inherited upstreamconsole.log('node.isClose', ...)line, so a future maintainer does not mistake it for local debug code or "fix" it silently.
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.
…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.
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.