- sanitizer XSS serveur (markdown + page de partage) [BUG-021/022] - rate-limit/lockout MFA [BUG-023] - isolation vaults par segments [BUG-024] - caps regex ReDoS [BUG-025] - SSRF webhooks + secrets externalises [BUG-026] - rotation/revocation des jetons [BUG-027] - politique de mot de passe + invalidation sessions [BUG-028] - verrous users.json [BUG-029] - IP reelle dans les audits [BUG-030] - rate-limit par compte [BUG-031] - symlinks hors vault ignores [BUG-032] - recherche simple via inverted index [BUG-033] - token en memoire + cookie HttpOnly, CSP durcie [BUG-034] Tests: pytest 961 passed / 6 skipped, ruff 0, mypy 0, frontend vert.
68 lines
2.2 KiB
Python
68 lines
2.2 KiB
Python
"""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
|