From 28c00d9d53a05661534b816bd9ab93113b926678 Mon Sep 17 00:00:00 2001 From: nandgator <38415384+nandgator@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:43:44 +0530 Subject: [PATCH] Require ASCII digits in Finnish, French and Indian identifiers fi_ssn(), fr_ssn(), ind_aadhar() and ind_pan() matched digits with \d and separators with \s, which for str patterns match any Unicode decimal digit or whitespace, and the checksums use int(), which accepts the same digits. An identifier with one digit swapped for its Arabic-Indic or fullwidth form, or a space for a no-break space, still validated. Add re.ASCII to the four patterns. --- src/validators/i18n/fi.py | 2 +- src/validators/i18n/fr.py | 2 +- src/validators/i18n/ind.py | 4 ++-- tests/i18n/test_fi.py | 6 ++++++ tests/i18n/test_fr.py | 13 +++++++++++++ tests/i18n/test_ind.py | 14 ++++++++++++++ 6 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/validators/i18n/fi.py b/src/validators/i18n/fi.py index 0ccda256..b42e1a58 100644 --- a/src/validators/i18n/fi.py +++ b/src/validators/i18n/fi.py @@ -26,7 +26,7 @@ def _ssn_pattern(ssn_check_marks: str): (?P[ABCDEFYXWVU+-]) (?P(\d{{3}})) (?P[{check_marks}])$""".format(check_marks=ssn_check_marks), - re.VERBOSE, + re.VERBOSE | re.ASCII, ) diff --git a/src/validators/i18n/fr.py b/src/validators/i18n/fr.py index e7c29332..5552ef61 100644 --- a/src/validators/i18n/fr.py +++ b/src/validators/i18n/fr.py @@ -20,7 +20,7 @@ def _ssn_pattern(): r"\s(\d{2,3})" # town of birth r"\s(\d{3})" # registration number r"(?:\s(\d{2}))?$", # control key (may or may not be provided) - re.VERBOSE, + re.VERBOSE | re.ASCII, ) diff --git a/src/validators/i18n/ind.py b/src/validators/i18n/ind.py index 29a58e2d..87d6d1d1 100644 --- a/src/validators/i18n/ind.py +++ b/src/validators/i18n/ind.py @@ -24,7 +24,7 @@ def ind_aadhar(value: str): (Literal[True]): If `value` is a valid aadhar card number. (ValidationError): If `value` is an invalid aadhar card number. """ - return re.fullmatch(r"^[2-9]{1}\d{3}\s\d{4}\s\d{4}$", value) + return re.fullmatch(r"^[2-9]{1}\d{3}\s\d{4}\s\d{4}$", value, re.ASCII) @validator @@ -44,4 +44,4 @@ def ind_pan(value: str): (Literal[True]): If `value` is a valid PAN card number. (ValidationError): If `value` is an invalid PAN card number. """ - return re.fullmatch(r"[A-Z]{5}\d{4}[A-Z]{1}", value) + return re.fullmatch(r"[A-Z]{5}\d{4}[A-Z]{1}", value, re.ASCII) diff --git a/tests/i18n/test_fi.py b/tests/i18n/test_fi.py index 08daef4f..ffbe206e 100644 --- a/tests/i18n/test_fi.py +++ b/tests/i18n/test_fi.py @@ -102,3 +102,9 @@ def test_ssn_validates_birth_date_in_its_century(birth_date: str, century: str, checksum = "0123456789ABCDEFHJKLMNPRSTUVWXY"[int(birth_date + serial) % 31] value = birth_date + century + serial + checksum assert bool(fi_ssn(value)) is valid + + +@pytest.mark.parametrize("value", ["1\u06631052-308T", "1310\u06652-308T", "131052-3\u06608T"]) +def test_returns_failed_validation_on_non_ascii_digits_in_ssn(value: str): + """Test returns failed validation when a script other than ASCII supplies the digits.""" + assert isinstance(fi_ssn(value), ValidationError) diff --git a/tests/i18n/test_fr.py b/tests/i18n/test_fr.py index 30c1588a..f25c87c5 100644 --- a/tests/i18n/test_fr.py +++ b/tests/i18n/test_fr.py @@ -85,3 +85,16 @@ def test_returns_true_on_valid_department(value: Union[str, int]): def test_returns_failed_validation_on_invalid_department(value: Union[str, int]): """Test returns failed validation on invalid department.""" assert isinstance(fr_department(value), ValidationError) + + +@pytest.mark.parametrize( + "value", + [ + "1 \u06684 12 76 451 089 46", # Arabic-Indic digit + "1 84 12 76 451 089 \uff146", # fullwidth digit + "1\u00a084 12 76 451 089 46", # no-break space + ], +) +def test_returns_failed_validation_on_non_ascii_digits_or_spaces_in_ssn(value: str): + """Test returns failed validation on digits or spaces outside ASCII.""" + assert isinstance(fr_ssn(value), ValidationError) diff --git a/tests/i18n/test_ind.py b/tests/i18n/test_ind.py index 36aae6d1..9ee64028 100644 --- a/tests/i18n/test_ind.py +++ b/tests/i18n/test_ind.py @@ -32,3 +32,17 @@ def test_returns_true_on_valid_ind_pan(value: str): def test_returns_failed_validation_on_invalid_ind_pan(value: str): """Test returns failed validation on invalid ind pan.""" assert isinstance(ind_pan(value), ValidationError) + + +@pytest.mark.parametrize( + "value", ["3\u066675 9834 6012", "3675 9834 601\uff12", "3675\u00a09834 6012"] +) +def test_returns_failed_validation_on_non_ascii_digits_or_spaces_in_ind_aadhar(value: str): + """Test returns failed validation on digits or spaces outside ASCII.""" + assert isinstance(ind_aadhar(value), ValidationError) + + +@pytest.mark.parametrize("value", ["ABCDE\u0669999K", "ABCDE999\uff19K"]) +def test_returns_failed_validation_on_non_ascii_digits_in_ind_pan(value: str): + """Test returns failed validation when a script other than ASCII supplies the digits.""" + assert isinstance(ind_pan(value), ValidationError)