Skip to content

Commit ec088c7

Browse files
DevKaurav-14nandgator
authored andcommitted
Look up the IDNA-encoded TLD when consider_tld is set
The IANA list stores internationalized TLDs in punycode (XN--P1AI), but domain() checked the raw Unicode TLD against it. Unicode domains such as пример.рф were rejected with consider_tld=True while their punycode form was accepted. hostname() and url() inherit this through domain(). Run the TLD check after IDNA encoding, on the encoded TLD.
1 parent aa1986b commit ec088c7

4 files changed

Lines changed: 61 additions & 3 deletions

File tree

‎src/validators/domain.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,6 @@ def domain(
9494
if not value:
9595
return False
9696

97-
if consider_tld and not _IanaTLD.check(value.rstrip(".").rsplit(".", 1)[-1].upper()):
98-
return False
99-
10097
try:
10198
if _relies_on_idna_mapping(value):
10299
return False
@@ -114,6 +111,11 @@ def domain(
114111
# ref: RFC 1035, presentation form is limited to 253 characters
115112
return False
116113

114+
# IANA lists IDN TLDs in their ASCII (punycode) form,
115+
# so look up the encoded TLD, not the raw one.
116+
if consider_tld and not _IanaTLD.check(encoded.rstrip(".").rsplit(".", 1)[-1].upper()):
117+
return False
118+
117119
return not re.search(r"\s|__+", value) and re.match(
118120
# First character of the domain
119121
rf"^(?:[a-z0-9{service_record}]"

‎tests/test_domain.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ def test_returns_true_on_valid_domain(value: str, rfc_1034: bool, rfc_2782: bool
4949
("somerandomexample.onion", True, False, False),
5050
# delegated 2026-07-23
5151
("somerandomexample.web", True, False, False),
52+
("пример.рф", True, False, False),
53+
("пример.рф.", True, True, False),
54+
("例子.中国", True, False, False),
5255
],
5356
)
5457
def test_returns_true_on_valid_top_level_domain(
@@ -104,6 +107,7 @@ def test_returns_failed_validation_on_invalid_domain(value: str, rfc_1034: bool,
104107
("somerandomexample.xn-n-fiqs8s", True, False, False),
105108
# retired from the root zone
106109
("somerandomexample.goo", True, False, False),
110+
("пример.рфы", True, False, False),
107111
],
108112
)
109113
def test_returns_failed_validation_invalid_top_level_domain(

‎tests/test_hostname.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,29 @@ def test_returns_failed_validation_on_idna_mapped_lookalike_hostname(value: str)
145145
def test_returns_failed_validation_on_ipv4_lookalike_hostname(value: str):
146146
"""Test returns failed validation on dotted names whose last label is non-ASCII digits."""
147147
assert isinstance(hostname(value), ValidationError)
148+
149+
150+
@pytest.mark.parametrize(
151+
"value",
152+
[
153+
"example.com:443",
154+
"xn--e1afmkfd.xn--p1ai",
155+
"пример.рф",
156+
"пример.рф:8080",
157+
],
158+
)
159+
def test_returns_true_on_valid_top_level_domain_hostname(value: str):
160+
"""Test returns true on hostname with a TLD allowed by IANA."""
161+
assert hostname(value, consider_tld=True)
162+
163+
164+
@pytest.mark.parametrize(
165+
"value",
166+
[
167+
"example.flat:443",
168+
"пример.рфы",
169+
],
170+
)
171+
def test_returns_failed_validation_on_invalid_top_level_domain_hostname(value: str):
172+
"""Test returns failed validation on hostname with a TLD not allowed by IANA."""
173+
assert isinstance(hostname(value, consider_tld=True), ValidationError)

‎tests/test_url.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,29 @@ def test_returns_failed_validation_on_idna_mapped_lookalike_url(value: str):
273273
def test_returns_failed_validation_on_ipv4_lookalike_url(value: str, private: Optional[bool]):
274274
"""Test returns failed validation on hosts that only look like an IPv4 address."""
275275
assert isinstance(url(value, private=private), ValidationError)
276+
277+
278+
@pytest.mark.parametrize(
279+
"value",
280+
[
281+
"https://example.com/path",
282+
"https://xn--e1afmkfd.xn--p1ai/",
283+
"https://пример.рф/",
284+
"http://例子.中国:8080/index.html",
285+
],
286+
)
287+
def test_returns_true_on_valid_top_level_domain_url(value: str):
288+
"""Test returns true on url with a TLD allowed by IANA."""
289+
assert url(value, consider_tld=True)
290+
291+
292+
@pytest.mark.parametrize(
293+
"value",
294+
[
295+
"https://example.flat/",
296+
"https://пример.рфы/",
297+
],
298+
)
299+
def test_returns_failed_validation_on_invalid_top_level_domain_url(value: str):
300+
"""Test returns failed validation on url with a TLD not allowed by IANA."""
301+
assert isinstance(url(value, consider_tld=True), ValidationError)

0 commit comments

Comments
 (0)