Skip to content

fix: literalize single-branch extglobs with a leading bare quantifier (CVE-2026-112366) - #238

Open
rborlone wants to merge 1 commit into
micromatch:masterfrom
rborlone:fix/redos-leading-extglob-quantifier-cve-2026-112366
Open

rborlone wants to merge 1 commit into
micromatch:masterfrom
rborlone:fix/redos-leading-extglob-quantifier-cve-2026-112366

Conversation

@rborlone

Copy link
Copy Markdown

Summary

Fixes CVE-2026-112366: a ReDoS vulnerability in repeated extglob parsing.

analyzeRepeatedExtglob (the function that decides whether a +(...)/*(...) extglob is safe to compile as-is, or must be literalized) only flagged a pattern as risky when:

  • the body had multiple branches (branches.length > 1), or
  • a branch was itself a fully parenthesized nested extglob (*(...)) that it could reduce or whose recursion depth it could count.

A single-branch body that starts with a bare (non-parenthesized) * or ? followed by more content — e.g. the *a in +(*a) — fell through both checks. It compiled straight through to a nested-quantifier regex ((?:[^/]*?a)+), which is prone to catastrophic backtracking: matching it against a short crafted string (e.g. many as with no trailing a) blocks the event loop.

makeRe('+(*a)').source
// before: ^(?:(?=.)(?:[^/]*?a)+)$   <- catastrophic backtracking
// after:  ^(?:\+\(\*a\))$           <- literalized, same as other risky extglobs

Fix

Adds hasUnsafeLeadingQuantifier in lib/parse.js, called alongside the existing recursion-depth check inside analyzeRepeatedExtglob's per-branch loop. It flags a branch as risky when it starts with a run of bare */? quantifier characters that is followed by further content, since there's no fixed prefix anchoring where the quantifier ends.

The check is intentionally scoped narrowly, to avoid regressing existing, already-tested behavior:

  • A bare quantifier with nothing after it (+(*)) is left alone — it's not ambiguous in the same way, and is already covered by an existing test (should preserve non-risky extglobs by default).
  • A quantifier anchored by a literal prefix (+(a*), or a quantifier appearing later in the branch behind other content, as in the existing *(W*, *) test in test/special-characters.js) is left alone — the literal prefix removes the ambiguity, and this shape was already relied upon by existing tests.
  • A *(...)/?(...) nested extglob group opener is not treated as a bare quantifier; its own contents continue to be analyzed separately by the existing recursion/char-class-reduction logic above.

Testing

  • Added two test cases to test/options.maxExtglobRecursion.js:
    • +(*a), *(*a), and +(a|*a) are literalized and no longer match a long crafted string that would trigger backtracking.
    • +(*) and +(a*) continue to work as normal globs (no regression).
  • Ran the full existing test suite (npm run mocha) — all 1998 pre-existing tests pass, plus the 2 new ones (2000 total).

Known limitation

A non-reducible nested extglob group followed by trailing content in the same branch (e.g. +(*(ab)c)) is a structurally similar shape that is not covered by this patch, since it wasn't part of the originally reported CVE and widening the check further risked more false positives without being able to verify the exact backtracking behavior empirically. Flagging it, if desired, would need a separate, carefully-scoped follow-up.

…CVE-2026-112366)

analyzeRepeatedExtglob only flagged a repeated extglob (+(...)/*(...)) as
risky when the body had multiple branches, or when a branch was itself a
fully parenthesized nested extglob. A single branch that starts with a bare
(non-parenthesized) * or ? followed by more content, e.g. the *a in +(*a),
slipped through both checks and compiled to a nested-quantifier regex
(e.g. (?:[^/]*?a)+) that is prone to catastrophic backtracking.

Add hasUnsafeLeadingQuantifier to catch this shape and literalize it like
the other risky cases. The check is scoped to a *leading* bare quantifier
with trailing content, so it doesn't affect patterns where the quantifier
has nothing after it (+(*)) or is anchored by a literal prefix (+(a*),
*(W*, *)), which are not ambiguous in the same way and already have test
coverage relying on them working as normal globs.
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