Skip to content

Commit ce8ada1

Browse files
committed
Reject all-numeric IDN top-level labels in domain()
RFC 1123 (2.1) and RFC 3696 (2) require the top-level label to not be all-numeric, so a host name never has the dotted-decimal form #.#.#.#. domain() enforced this by requiring the encoded TLD to end in a letter, but a label of non-ASCII digits encodes to a letter-final A-label, so "10.0.0.\u0661" (Arabic-Indic one) validated as a domain, and through hostname() and url() as a host that looks like a private IPv4 address. Reject a value whose last label is made only of decimal digits from any script.
1 parent f5d227e commit ce8ada1

4 files changed

Lines changed: 44 additions & 0 deletions

File tree

‎src/validators/domain.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ def domain(
101101
if _relies_on_idna_mapping(value):
102102
return False
103103

104+
if value.rstrip(".").rsplit(".", 1)[-1].isdecimal():
105+
# ref: RFC 1123 (2.1) and RFC 3696 (2), a TLD is not all-numeric. An
106+
# all-digit IDN label like "\u0661" is, but encodes to a letter-final A-label.
107+
return False
108+
104109
service_record = r"_" if rfc_2782 else ""
105110
trailing_dot = r"\.?$" if rfc_1034 else r"$"
106111

‎tests/test_domain.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,29 @@ def test_returns_failed_validation_on_idna_mapped_lookalike(value: str, consider
203203
def test_returns_true_on_idn_without_lookalike_mapping(value: str):
204204
"""Test returns true on internationalized names that need no lossy mapping."""
205205
assert domain(value)
206+
207+
208+
@pytest.mark.parametrize(
209+
"value",
210+
[
211+
"10.0.0.\u0661", # Arabic-Indic one; looks like 10.0.0.1
212+
"example.\u0661\u0662\u0663",
213+
"example.\u0968", # Devanagari two
214+
"123.5.77.\u06688", # Arabic-Indic and ASCII digits
215+
],
216+
)
217+
@pytest.mark.parametrize("rfc_1034", [False, True])
218+
def test_returns_failed_validation_on_all_numeric_idn_tld(value: str, rfc_1034: bool):
219+
"""Test returns failed validation on a TLD made only of digits from any script."""
220+
assert isinstance(domain(value, rfc_1034=rfc_1034), ValidationError)
221+
222+
223+
def test_returns_failed_validation_on_all_numeric_idn_tld_with_trailing_dot():
224+
"""Test returns failed validation on an all-numeric IDN TLD followed by a trailing dot."""
225+
assert isinstance(domain("example.\u0661.", rfc_1034=True), ValidationError)
226+
227+
228+
@pytest.mark.parametrize("value", ["\u0661\u0662.com", "example.\u0661a", "1.example.com"])
229+
def test_returns_true_on_digits_outside_or_mixed_in_tld(value: str):
230+
"""Test returns true when digits appear only in other labels or alongside letters."""
231+
assert domain(value)

‎tests/test_hostname.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,9 @@ def test_returns_failed_validation_on_non_ascii_simple_hostname(value: str):
139139
def test_returns_failed_validation_on_idna_mapped_lookalike_hostname(value: str):
140140
"""Test returns failed validation on hostnames that only IDNA 2003 mapping makes valid."""
141141
assert isinstance(hostname(value), ValidationError)
142+
143+
144+
@pytest.mark.parametrize("value", ["10.0.0.\u0661", "123.5.77.\u06688:31000"])
145+
def test_returns_failed_validation_on_ipv4_lookalike_hostname(value: str):
146+
"""Test returns failed validation on dotted names whose last label is non-ASCII digits."""
147+
assert isinstance(hostname(value), ValidationError)

‎tests/test_url.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,10 @@ def test_url_accepts_percent_encoded_controls(encoded: str, template: str):
266266
def test_returns_failed_validation_on_idna_mapped_lookalike_url(value: str):
267267
"""Test returns failed validation on url hosts that only IDNA 2003 mapping makes valid."""
268268
assert isinstance(url(value, simple_host=True), ValidationError)
269+
270+
271+
@pytest.mark.parametrize("value", ["http://10.0.0.\u0661/", "https://192.168.1.\u0661:8080/"])
272+
@pytest.mark.parametrize("private", [None, False, True])
273+
def test_returns_failed_validation_on_ipv4_lookalike_url(value: str, private: Optional[bool]):
274+
"""Test returns failed validation on hosts that only look like an IPv4 address."""
275+
assert isinstance(url(value, private=private), ValidationError)

0 commit comments

Comments
 (0)