"""Regex safety helpers (BUG-025). User-supplied regular expressions are applied to large amounts of indexed content. Python's :mod:`re` has no timeout, so a malicious pattern such as ``(a+)+$`` can pin a CPU for a long time (ReDoS). Without adding a native dependency we mitigate by: * bounding the pattern length, * rejecting nested-quantifier constructs (the classic catastrophic form), * capping the amount of text a single regex pass may scan, * capping the number of matches collected. """ from __future__ import annotations import re __all__ = [ "MAX_PATTERN_LENGTH", "MAX_REGEX_CONTENT", "MAX_REGEX_MATCHES", "truncate_for_regex", "validate_regex", ] MAX_PATTERN_LENGTH = 500 MAX_REGEX_CONTENT = 200_000 MAX_REGEX_MATCHES = 1000 # A quantified group whose body already contains a quantifier, immediately # followed by another quantifier: ``(a+)+``, ``(.*)*``, ``(a+){2,}``, ... _NESTED_QUANTIFIER_RE = re.compile(r"\([^()]*[+*][^()]*\)\s*(?:[+*?]|\{)") # Backreferences combined with quantifiers are a common ReDoS vector too. _BACKREF_QUANTIFIER_RE = re.compile(r"\\[1-9][0-9]*\s*(?:[+*]|\{)") def validate_regex(pattern: str) -> str: """Validate a user-supplied regex against the safety policy. Args: pattern: Raw regex pattern. Returns: The pattern unchanged when acceptable. Raises: ValueError: When the pattern is empty, too long, or uses a construct known to cause catastrophic backtracking. """ if not pattern: raise ValueError("Expression régulière vide") if len(pattern) > MAX_PATTERN_LENGTH: raise ValueError(f"Expression régulière trop longue (max {MAX_PATTERN_LENGTH})") if _NESTED_QUANTIFIER_RE.search(pattern) or _BACKREF_QUANTIFIER_RE.search(pattern): raise ValueError("Expression régulière refusée (quantificateurs imbriqués)") try: re.compile(pattern) except re.error as e: raise ValueError(f"Expression régulière invalide : {e}") from e return pattern def truncate_for_regex(content: str, limit: int = MAX_REGEX_CONTENT) -> str: """Return at most *limit* characters to bound a single regex pass.""" if content and len(content) > limit: return content[:limit] return content