From f63135a20037a575899cb508aa7b4cf4825fd2d1 Mon Sep 17 00:00:00 2001 From: Enes Uygur Date: Wed, 28 Oct 2026 12:27:55 +0300 Subject: [PATCH] Reject non-ASCII case equivalents in country and currency codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With ignore_case=True, country_code() and currency() compared value.upper() against the ISO lists. str.upper() maps letters such as dotless i (U+0131), long s (U+017F) and the fi ligature (U+FB01) onto ASCII, so "ıt", "ıta", "fiN" and "uſd" were accepted as IT, ITA, FIN and USD. Require ASCII input before case-insensitive matching. --- src/validators/country.py | 6 ++++-- tests/test_country.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/validators/country.py b/src/validators/country.py index f220c77f..1d2c6508 100644 --- a/src/validators/country.py +++ b/src/validators/country.py @@ -299,7 +299,8 @@ def country_code(value: str, /, *, iso_format: str = "auto", ignore_case: bool = if not value: return False - if not (1 < len(value) < 4): + # str.upper() would map e.g. dotless i and long s onto ASCII letters + if not (1 < len(value) < 4) or not value.isascii(): return False if iso_format == "auto" and (iso_format := _get_code_type(value)) == "invalid": @@ -349,7 +350,8 @@ def currency(value: str, /, *, skip_symbols: bool = True, ignore_case: bool = Fa if not skip_symbols and value in _currency_symbols: return True - if len(value) != 3: + # str.upper() would map e.g. dotless i and long s onto ASCII letters + if len(value) != 3 or not value.isascii(): return False return value.upper() in _currency_iso4217 if ignore_case else value in _currency_iso4217 diff --git a/tests/test_country.py b/tests/test_country.py index 1485d265..58507013 100644 --- a/tests/test_country.py +++ b/tests/test_country.py @@ -55,6 +55,22 @@ def test_returns_failed_validation_on_invalid_country_code(value: str, iso_forma assert isinstance(country_code(value, iso_format=iso_format), ValidationError) +@pytest.mark.parametrize( + ("value", "iso_format"), + [ + ("\u0131t", "auto"), + ("\u017fe", "alpha2"), + ("\u0131ta", "auto"), + ("\ufb01N", "alpha3"), + ], +) +def test_returns_failed_validation_on_non_ascii_case_equivalent_country_code( + value: str, iso_format: str +): + """Test returns failed validation when Unicode case mapping yields an ASCII code.""" + assert isinstance(country_code(value, iso_format=iso_format, ignore_case=True), ValidationError) + + @pytest.mark.parametrize( ("value", "skip_symbols", "ignore_case"), [("$", False, False), ("⃀", False, False), ("৳", False, False), ("uSd", True, True)], @@ -73,6 +89,8 @@ def test_returns_true_on_valid_currency(value: str, skip_symbols: bool, ignore_c ("⃀৳", False, False), ("uSd", True, False), ("Bucks", True, True), + ("u\u017fd", True, True), + ("\u0131ls", True, True), ], ) def test_returns_failed_validation_invalid_currency(