/** * RegexGuard - Static ReDoS guard for user-supplied regex patterns. * * Rejects patterns that are too long, contain nested unbounded quantifiers * (`(a+)+`), or have overlapping alternation branches inside an unboundedly * quantified group (`(a|aa)+`). Bounded quantifiers like `{2,9}` are allowed. * * @class RegexGuard * @example * const safe = RegexGuard.compileRegex("(a+)b", ""); * const guarded = RegexGuard.compileRegex("(a+)+$", ""); // throws */ export default class RegexGuard { /** Upper bound on pattern length. */ static readonly MAX_PATTERN_LENGTH = 512; /** * Compiles a user-supplied pattern into a RegExp after proving it is safe. * @param pattern - The regex source to compile. * @param flags - Optional regex flags (e.g. "i"). * @returns A compiled RegExp. * @throws Error if the pattern is empty, too long, or prone to catastrophic backtracking. */ static compileRegex(pattern: string, flags?: string): RegExp; /** * Returns true when the pattern does not contain the catastrophic * nested-quantifier shape. Pure check - callers decide what to do. * @param pattern - The regex source to inspect. * @returns true if the pattern is considered safe, false otherwise. */ static isSafePattern(pattern: string): boolean; /** * Static scan for the catastrophic-backtracking shape. * * Two classes are rejected: * 1. Nested unbounded quantifiers: a group that contains an internally * quantified atom and is itself unboundedly quantified, e.g. `(a+)+`. * 2. Overlapping alternation inside an unboundedly quantified group: two * branches where one is a strict prefix of another, e.g. `(a|aa)+` - * the parser cannot decide where a branch ends, causing exponential work. * * @param pattern - The regex source to inspect. * @throws Error describing the dangerous construct when detected. */ static assertSafePattern(pattern: string): void; /** * Splits a group body on top-level alternation operators and reports whether * any two branches overlap, i.e. one is a strict prefix of another. Branch * overlap on an unboundedly quantified group is what produces exponential * backtracking in patterns like `(a|aa)+`. * @param body - The characters between a group's '(' and ')'. * @returns true when an overlapping branch pair is found. */ private static hasOverlappingAlternation; /** * Strips the special group marker (`?:`, `?=`, `?!`, `?<=`, `?`) * from a group body so branch analysis sees the actual alternation content. * @param body - The raw text between '(' and ')'. * @returns The body without its leading marker, if one was present. */ private static stripGroupMarker; /** * Splits a group body on '|' operators that are not nested inside a * sub-group or character class. * @param body - The group's raw body text. * @returns The top-level alternation branches (at least one element). */ private static splitTopLevel; /** * Determines what kind of quantifier (if any) follows a closing parenthesis. * @param pattern - The full regex source. * @param closeIndex - Index of the ')' that just closed a group. * @returns "none" when no quantifier follows, "unbounded" for `*`, `+`, or an * open-ended `{n,}` range, otherwise "bounded". */ private static peekGroupQuantifier; /** * Parses a `{...}` quantifier starting at the given index. * @param pattern - The full regex source. * @param openIndex - Index of the '{'. * @returns true for an unbounded `{n,}` range, false for a bounded `{n}` or * `{n,m}`, and null when the character is not actually a quantifier * (e.g. a literal brace inside a larger construct). */ private static isUnboundedRange; }