The raw regex source the caller wants to compile.
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
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_questionsruns the caller'spatternagainst every line of every note, so an unvalidated regex is a remote DoS on a bearer-authenticatedserve-http(the tool is always registered).Catches four catastrophic shapes — each is an unbounded-quantified group (
(...)+,(...)*,(...){n,},(...){0,BIG}) whose body is "dangerous":(a+)+,(.*)*).(a|a)+,(a|ab)*,(.|a)+,(a|A)+(/i),(\\x61|a)+(escape alias),(a?b|b)+(optional leading atom). Decided byalternationBodyAmbiguousoverleadingAtomSet(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.(a?)+,(\\s*)*,()+), so each repetition can consume nothing.(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.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 byframeAdjacentOverlap; 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.tsfuzz harness, which times a realexecagainst 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.