From e9aa8faa7256e0b7b14fd916b150934e8646ccc1 Mon Sep 17 00:00:00 2001 From: uwezkhan Date: Wed, 17 Jun 2026 17:01:35 +0530 Subject: [PATCH 1/2] fix(analyzer): stop quadratic backtracking in in_pan low pattern --- .../india/in_pan_recognizer.py | 2 +- .../tests/test_in_pan_recognizer.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/india/in_pan_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/india/in_pan_recognizer.py index 700b0bd1ed..f38e17076e 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/india/in_pan_recognizer.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/india/in_pan_recognizer.py @@ -38,7 +38,7 @@ class InPanRecognizer(PatternRecognizer): ), Pattern( "PAN (Low)", - r"\b((?=.*?[a-zA-Z])(?=.*?[0-9]{4})[\w@#$%^?~-]{10})\b", + r"\b((?=[\w@#$%^?~-]*?[a-zA-Z])(?=[\w@#$%^?~-]*?[0-9]{4})[\w@#$%^?~-]{10})\b", 0.01, ), ] diff --git a/presidio-analyzer/tests/test_in_pan_recognizer.py b/presidio-analyzer/tests/test_in_pan_recognizer.py index 2c7b136255..d7c55b68ab 100644 --- a/presidio-analyzer/tests/test_in_pan_recognizer.py +++ b/presidio-analyzer/tests/test_in_pan_recognizer.py @@ -1,3 +1,5 @@ +import time + import pytest from tests import assert_result @@ -47,3 +49,25 @@ def test_when_pan_in_text_then_all_pans_found( expected_position[1], expected_score, ) + + +def test_low_confidence_pan_with_embedded_digits_still_detected(recognizer, entities): + # The low-confidence pattern keeps matching a 10-char token that carries a + # letter and a four-digit run; scoping the lookaheads to the token class + # leaves this detection unchanged. + results = recognizer.analyze("A1111DFSFS", entities) + assert len(results) == 1 + assert_result(results[0], entities[0], 0, 10, 0.01) + + +def test_low_confidence_pattern_does_not_backtrack(recognizer, entities): + # The low-confidence pattern used `.*?` lookaheads that rescanned the whole + # remaining text at every word boundary, so a long boundary-rich string with + # no four-digit run cost time quadratic in its length. Matching must now be + # linear and finish well within the regex timeout. + text = "a " * 50000 + start = time.time() + results = recognizer.analyze(text, entities) + elapsed = time.time() - start + assert results == [] + assert elapsed < 10 From ba62a431ed90a262b9fa594964f5bf9fa6878252 Mon Sep 17 00:00:00 2001 From: Uwez Khan Date: Thu, 2 Jul 2026 16:38:02 +0530 Subject: [PATCH 2/2] test(analyzer): use perf_counter for in_pan backtracking guard time.time() is wall-clock and non-monotonic, which can make the timing guard flaky. Switch to perf_counter for a monotonic, higher-resolution measurement. Signed-off-by: Uwez Khan --- presidio-analyzer/tests/test_in_pan_recognizer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presidio-analyzer/tests/test_in_pan_recognizer.py b/presidio-analyzer/tests/test_in_pan_recognizer.py index d7c55b68ab..c87ed95cb3 100644 --- a/presidio-analyzer/tests/test_in_pan_recognizer.py +++ b/presidio-analyzer/tests/test_in_pan_recognizer.py @@ -66,8 +66,8 @@ def test_low_confidence_pattern_does_not_backtrack(recognizer, entities): # no four-digit run cost time quadratic in its length. Matching must now be # linear and finish well within the regex timeout. text = "a " * 50000 - start = time.time() + start = time.perf_counter() results = recognizer.analyze(text, entities) - elapsed = time.time() - start + elapsed = time.perf_counter() - start assert results == [] assert elapsed < 10