Skip to content

Commit 70be4b7

Browse files
authored
fix(pii): bound custom-pattern matching time per request (#9584)
Presidio's REGEX_TIMEOUT_SECONDS bounds one custom pattern on one text, so a batch request could cost texts x patterns x that timeout (2,000 texts x 20 patterns x 2s) on a single handler thread. Charge every custom-pattern match to a per-request 60s budget and reject the request with a 400 once it is spent.
1 parent 29dd567 commit 70be4b7

2 files changed

Lines changed: 42 additions & 3 deletions

File tree

‎apps/pii/server.py‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,13 +391,49 @@ def custom_operators(patterns: list[CustomPattern] | None) -> dict[str, dict[str
391391
}
392392

393393

394+
# Seconds one request may spend matching its custom patterns, summed over every
395+
# pattern and text. REGEX_TIMEOUT_SECONDS bounds only one pattern on one text, so
396+
# without this a batch could cost texts x patterns x that timeout.
397+
CUSTOM_PATTERN_BUDGET_SECONDS = 60
398+
399+
400+
class _MatchBudget:
401+
"""Custom-pattern matching seconds left for one request, shared by its
402+
recognizers. A request is analyzed on a single thread, so no lock is needed."""
403+
404+
def __init__(self, seconds: float) -> None:
405+
self.remaining = seconds
406+
407+
408+
class BudgetedPatternRecognizer(PatternRecognizer):
409+
"""Custom-pattern recognizer that charges its matching time to the request's
410+
budget and fails the request with a 400 once the budget is spent."""
411+
412+
def __init__(self, budget: _MatchBudget, **kwargs: Any) -> None:
413+
super().__init__(**kwargs)
414+
self._budget = budget
415+
416+
def analyze(self, *args: Any, **kwargs: Any) -> list[RecognizerResult]:
417+
started = time.perf_counter()
418+
results = super().analyze(*args, **kwargs)
419+
self._budget.remaining -= time.perf_counter() - started
420+
if self._budget.remaining < 0:
421+
raise HTTPException(
422+
status_code=400,
423+
detail=f"Custom patterns exceeded the {CUSTOM_PATTERN_BUDGET_SECONDS}s "
424+
"matching budget for this request",
425+
)
426+
return results
427+
428+
394429
def build_custom_recognizers(
395430
patterns: list[CustomPattern] | None, language: str
396431
) -> tuple[list[PatternRecognizer], list[str]]:
397432
"""Ad-hoc PatternRecognizers + their entity ids for the given custom patterns.
398433
399434
Each regex is precompiled so a malformed pattern fails fast as a 400 rather
400435
than surfacing later as an opaque analyze-time 500."""
436+
budget = _MatchBudget(CUSTOM_PATTERN_BUDGET_SECONDS)
401437
recognizers: list[PatternRecognizer] = []
402438
entity_ids: list[str] = []
403439
for i, p in enumerate(patterns or []):
@@ -409,7 +445,8 @@ def build_custom_recognizers(
409445
) from exc
410446
entity = f"{CUSTOM_ENTITY_PREFIX}{i}"
411447
recognizers.append(
412-
PatternRecognizer(
448+
BudgetedPatternRecognizer(
449+
budget,
413450
supported_entity=entity,
414451
# Score 1.0 so a user's explicit pattern wins any overlap with a
415452
# built-in detector (e.g. spaCy tagging "EMP-123456" as ORGANIZATION

‎apps/sim/lib/guardrails/validate-regex.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ export interface RegexPatternValidation {
3232
*
3333
* Nor could a screen here be made sound: these patterns execute in Presidio's
3434
* Python engine, which backtracks on shapes RE2 accepts, so RE2-representability
35-
* says nothing about their runtime there. Presidio's own request timeout is the
36-
* real bound — note it fails open on timeout, leaving PII unredacted.
35+
* says nothing about their runtime there. Presidio bounds them instead: a
36+
* per-match timeout, which fails open (the timed-out pattern is skipped, leaving
37+
* its PII unredacted), and a per-request matching budget, which rejects the
38+
* request.
3739
*
3840
* Anything that matches a caller-supplied pattern *in this process* must use
3941
* `compileLinearRegex` from `@/lib/core/security/linear-regex` instead.

0 commit comments

Comments
 (0)