enquire-mcp API reference - v3.11.6-rc.2
    Preparing search index...

    Function isCatastrophicRegex

    • Conservative, dependency-free guard against catastrophic-backtracking (ReDoS) in a caller-supplied regex. V8's Irregexp engine backtracks, so (a+)+$, (.*)*, or (.*a){20} can freeze the event loop for seconds on a single long line. obsidian_open_questions runs the caller's pattern against every line of every note, so an unvalidated regex is a remote DoS on a bearer-authenticated serve-http (the tool is always registered).

      Catches four catastrophic shapes — each is an unbounded-quantified group ((...)+, (...)*, (...){n,}, (...){0,BIG}) whose body is "dangerous":

      1. Star height ≥ 2 — the body ALSO contains an unbounded quantifier ((a+)+, (.*)*).
      2. Ambiguous alternation (v3.9.0-rc.21/rc.24/rc.25) — the body's top-level branches' LEADING SETS intersect, so the backtracker can split one repetition many ways: (a|a)+, (a|ab)*, (.|a)+, (a|A)+ (/i), (\\x61|a)+ (escape alias), (a?b|b)+ (optional leading atom). Decided by alternationBodyAmbiguous over leadingAtomSet (a sound over-approximation), so a DISJOINT alternation ((a|b|c)+, (cat|dog)+, (a?b|c)+) stays accepted and a NON-quantified alternation ((?:a|b)\s*, the default pattern's shape) is never flagged.
      3. Nullable body (v3.9.0-rc.25) — the body can match empty ((a?)+, (\\s*)*, ()+), so each repetition can consume nothing.
      4. Variable-length body (v3.9.0-rc.25) — the body contains a variable-length quantifier ((a{2,5})+, (\\w[ba]{0,3})+, (a[ab]?)+); consecutive repetitions partition a long run exponentially. Gated on the OUTER quantifier being unbounded, so a BOUNDED outer ((.+){2,5}, ≤5 reps → polynomial) stays accepted.
      5. Adjacent overlapping quantifiers (v3.10.0-rc.36) — two ADJACENT atoms, each unbounded-quantified, whose match sets overlap, at ANY frame INCLUDING the TOP level: a*a*$, \\w*\\w*$, (a)*(a)*$. rc.21–rc.25 only evaluated the verdict when a QUANTIFIED GROUP closed, so a BARE top-level sequence (frame 0 is never popped) slipped — the rc.36 CRITICAL. Decided by frameAdjacentOverlap; a DISJOINT adjacency (a*b*$) stays accepted, and a .-greedy absorber tail (…\\s*\\s*(.+)$, the default's shape) is benign.

      Sound but conservative: it never UNDER-flags a real catastrophic shape (proven by the tests/redos-fuzz.test.ts fuzz harness, which times a real exec against the static verdict), but it MAY over-flag — (cat|car)+ (shared first char), (a?b)+ / (\\w+\\s)+ (variable but anchored). For a security guard, a rare false positive (caller simplifies / omits the override) beats a hung event loop.

      Honors char-classes ([...] contents are literal) and backslash escapes (\(, \+, \| are literals). Still best-effort — ReDoS detection is undecidable in general — so the caller also pairs it with MAX_QUESTION_PATTERN_LEN.

      Parameters

      • src: string

        The raw regex source the caller wants to compile.

      Returns boolean

      true if the pattern risks catastrophic backtracking.

      isCatastrophicRegex("(a+)+$");        // true  — nested unbounded quantifiers
      isCatastrophicRegex("(a|a)+$"); // true — ambiguous alternation under +
      isCatastrophicRegex("(a?b|b)+$"); // true — optional leading atom overlaps (rc.25)
      isCatastrophicRegex("(a?){25}"); // true — nullable body under repetition (rc.25)
      isCatastrophicRegex("(a{2,5})+"); // true — variable-length body under + (rc.25)
      isCatastrophicRegex("\\w*\\w*$"); // true — adjacent overlapping quantifiers (rc.36)
      isCatastrophicRegex("a*b*$"); // false — adjacent but DISJOINT (linear)
      isCatastrophicRegex("(a|b|c)+"); // false — disjoint alternation (linear)
      isCatastrophicRegex("(.+){2,5}"); // false — variable body but BOUNDED outer
      isCatastrophicRegex("^Q: (.+)$"); // false — single-level
      isCatastrophicRegex("(?:a|b)\\s*c"); // false — alternation NOT unbounded-quantified