From dd3a327b8e069647cc87d93bc65b7a171e1ee36f Mon Sep 17 00:00:00 2001 From: Shaisolaris <66793913+Shaisolaris@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:57:51 -0700 Subject: [PATCH] Reject non-ASCII digits in card_number, uuid and cron int() and str.isdecimal() accept digits of other scripts, so values such as the Arabic-Indic form of 4242424242424242 passed card_number, and uuid and cron accepted them too. Require ASCII input before those checks. --- src/validators/card.py | 3 ++- src/validators/cron.py | 11 ++++++++--- src/validators/uuid.py | 3 +++ tests/test_card.py | 6 ++++++ tests/test_cron.py | 14 ++++++++++++++ tests/test_uuid.py | 13 +++++++++++++ 6 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/validators/card.py b/src/validators/card.py index 484a9dc0..427d76d3 100644 --- a/src/validators/card.py +++ b/src/validators/card.py @@ -29,7 +29,8 @@ def card_number(value: str, /): (Literal[True]): If `value` is a valid generic card number. (ValidationError): If `value` is an invalid generic card number. """ - if not value: + if not value or not value.isascii(): + # int() also accepts digits of other scripts, which are not card numbers return False try: digits = list(map(int, value)) diff --git a/src/validators/cron.py b/src/validators/cron.py index 7afc4630..418db481 100644 --- a/src/validators/cron.py +++ b/src/validators/cron.py @@ -4,11 +4,16 @@ from .utils import validator +def _is_number(text: str): + # str.isdecimal() also accepts digits of other scripts, e.g. "\u0663". + return text.isascii() and text.isdecimal() + + def _validate_cron_item(item: str, min_val: int, max_val: int): # An item may carry a step, e.g. "*/5", "1-30/2" or "5/10". if "/" in item: base, _, step = item.partition("/") - if not step.isdecimal() or int(step) < 1: + if not _is_number(step) or int(step) < 1: return False else: base = item @@ -18,12 +23,12 @@ def _validate_cron_item(item: str, min_val: int, max_val: int): if "-" in base: parts = base.split("-") - if len(parts) != 2 or not parts[0].isdecimal() or not parts[1].isdecimal(): + if len(parts) != 2 or not _is_number(parts[0]) or not _is_number(parts[1]): return False start, end = int(parts[0]), int(parts[1]) return min_val <= start <= max_val and min_val <= end <= max_val and start <= end - if base.isdecimal(): + if _is_number(base): return min_val <= int(base) <= max_val return False diff --git a/src/validators/uuid.py b/src/validators/uuid.py index ca6b1ba0..d3406fae 100644 --- a/src/validators/uuid.py +++ b/src/validators/uuid.py @@ -35,6 +35,9 @@ def uuid(value: Union[str, UUID], /): return False if isinstance(value, UUID): return True + if isinstance(value, str) and not value.isascii(): + # int(..., 16) inside UUID() also accepts digits of other scripts + return False try: return UUID(value) or re.match( r"^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$", value diff --git a/tests/test_card.py b/tests/test_card.py index c17f5d95..c246618d 100644 --- a/tests/test_card.py +++ b/tests/test_card.py @@ -217,3 +217,9 @@ def test_returns_true_on_valid_mir(value: str): def test_returns_failed_on_valid_mir(value: str): """Test returns failed on invalid Mir card (other payment systems).""" assert isinstance(mir(value), ValidationError) + + +@pytest.mark.parametrize("value", ["\u0664242424242424242", "424242424242424\u0662"]) +def test_card_number_rejects_non_ascii_digits(value: str): + """Test card_number does not accept digits of other scripts.""" + assert isinstance(card_number(value), ValidationError) diff --git a/tests/test_cron.py b/tests/test_cron.py index 86723965..c92b9823 100644 --- a/tests/test_cron.py +++ b/tests/test_cron.py @@ -57,3 +57,17 @@ def test_returns_true_on_valid_cron(value: str): def test_returns_failed_validation_on_invalid_cron(value: str): """Test returns failed validation on invalid cron string.""" assert isinstance(cron(value), ValidationError) + + +@pytest.mark.parametrize( + "value", + [ + "\u0665 * * * *", + "*/\u0665 * * * *", + "1-\u0665 * * * *", + "* \u0661\u0662 * * *", + ], +) +def test_returns_failed_validation_on_non_ascii_digits_in_cron(value: str): + """Test returns failed validation when a script other than ASCII supplies the digits.""" + assert isinstance(cron(value), ValidationError) diff --git a/tests/test_uuid.py b/tests/test_uuid.py index b4f40d29..02b8b00b 100644 --- a/tests/test_uuid.py +++ b/tests/test_uuid.py @@ -37,3 +37,16 @@ def test_returns_true_on_valid_uuid(value: Union[str, UUID]): def test_returns_failed_validation_on_invalid_uuid(value: Union[str, UUID]): """Test returns failed validation on invalid uuid.""" assert isinstance(uuid(value), ValidationError) + + +@pytest.mark.parametrize( + ("value",), + [ + ("\u0662bc1c94f-0deb-43e9-92a1-4775189ec9f8",), + ("2bc1c94f-0deb-43e9-92a1-4775189ec9f\u0668",), + ("\u0662bc1c94f0deb43e992a14775189ec9f8",), + ], +) +def test_returns_failed_validation_on_non_ascii_digits_in_uuid(value: str): + """Test returns failed validation when a script other than ASCII supplies the digits.""" + assert isinstance(uuid(value), ValidationError)