From fbb5559656ab38c353d31e6ef1af4eda5aea4e3f Mon Sep 17 00:00:00 2001 From: John Kearney Date: Sun, 14 Jun 2026 20:32:04 -0500 Subject: [PATCH] fix(analyzer): stop UsSsnRecognizer from over-blocking 987-65-432X SSNs The invalidate_result() sample-SSN blocklist was matched with str.startswith(), and the literal "98765432" is only 8 digits (a truncation of the canonical fake "987654320"). The prefix match therefore invalidated the entire 987-65-4320 .. 987-65-4329 family -- ten distinct SSN-shaped values, including the widely-printed 987-65-4321 -- so real SSNs were dropped to no result and left unredacted. Split the two concerns: keep 000/666 as a never-issued area-number (first-group) check, and match the full 9-digit sample SSNs (123456789 / 987654320 / 078051120) by exact equality so they no longer over-block neighbours. NeMo Guardrails' PII rail inherits this recognizer, so the leak reached that deployed guardrail too. --- .../country_specific/us/us_ssn_recognizer.py | 10 ++++++--- .../tests/test_us_ssn_recognizer.py | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/us/us_ssn_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/us/us_ssn_recognizer.py index aec146f1dc..c3e60f76c4 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/us/us_ssn_recognizer.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/us/us_ssn_recognizer.py @@ -77,8 +77,12 @@ def invalidate_result(self, pattern_text: str) -> bool: # groups cannot be all zeros return True - for sample_ssn in ("000", "666", "123456789", "98765432", "078051120"): - if only_digits.startswith(sample_ssn): - return True + if only_digits[:3] in ("000", "666"): + # area number (first group) is never issued by the SSA + return True + + if only_digits in ("123456789", "987654320", "078051120"): + # canonical sample/placeholder SSNs published for examples + return True return False diff --git a/presidio-analyzer/tests/test_us_ssn_recognizer.py b/presidio-analyzer/tests/test_us_ssn_recognizer.py index b7b2aa0bf1..c0900b4753 100644 --- a/presidio-analyzer/tests/test_us_ssn_recognizer.py +++ b/presidio-analyzer/tests/test_us_ssn_recognizer.py @@ -27,6 +27,20 @@ def entities(): ("078.05.1123", 1, ((0, 11),), ((0.5, 0.6),),), ("078 05 1123", 1, ((0, 11),), ((0.5, 0.6),),), ("abc 078 05 1123 abc", 1, ((4, 15),), ((0.5, 0.6),),), + # The truncated "98765432" sample literal must not prefix-block the + # neighbouring 987-65-432X family; only the canonical 987-65-4320 sample + # is invalidated (by exact match), the rest are real SSN-shaped values + ("987-65-4321", 1, ((0, 11),), ((0.5, 0.6),),), + ("987-65-4322", 1, ((0, 11),), ((0.5, 0.6),),), + ("987-65-4323", 1, ((0, 11),), ((0.5, 0.6),),), + ("987-65-4324", 1, ((0, 11),), ((0.5, 0.6),),), + ("987-65-4325", 1, ((0, 11),), ((0.5, 0.6),),), + ("987-65-4326", 1, ((0, 11),), ((0.5, 0.6),),), + ("987-65-4327", 1, ((0, 11),), ((0.5, 0.6),),), + ("987-65-4328", 1, ((0, 11),), ((0.5, 0.6),),), + ("987-65-4329", 1, ((0, 11),), ((0.5, 0.6),),), + # a normal valid SSN is still detected + ("219-09-9999", 1, ((0, 11),), ((0.5, 0.6),),), # no match ("0780511201", 0, (), (),), ("078051120", 0, (), (),), @@ -35,6 +49,13 @@ def entities(): ("078-05-0000", 0, (), (),), ("078 00 1123", 0, (), (),), ("693-09.4444", 0, (), (),), + # canonical sample SSNs stay invalidated (now via exact match) + ("987-65-4320", 0, (), (),), + ("078-05-1120", 0, (), (),), + ("123-45-6789", 0, (), (),), + # never-issued area numbers (000/666) stay invalidated via the area check + ("000-12-3456", 0, (), (),), + ("666-12-3456", 0, (), (),), # fmt: on ], )