diff --git a/src/validators/email.py b/src/validators/email.py index 46cc5946..021adf76 100644 --- a/src/validators/email.py +++ b/src/validators/email.py @@ -67,7 +67,9 @@ def email( if ipv6_address or ipv4_address: if domain_part.startswith("[") and domain_part.endswith("]"): # ref: RFC 5321 - domain_part = domain_part.lstrip("[").rstrip("]") + domain_part = domain_part[1:-1] + if "[" in domain_part or "]" in domain_part: + return False else: return False diff --git a/src/validators/hostname.py b/src/validators/hostname.py index e34ab7e4..c6578271 100644 --- a/src/validators/hostname.py +++ b/src/validators/hostname.py @@ -37,7 +37,7 @@ def _port_validator(value: str): # with ipv6 host_seg, port_seg = value.rsplit(":", 1) if _port_regex().fullmatch(f":{port_seg}"): - return host_seg.lstrip("[").rstrip("]") + return host_seg if value.count(":") == 1: # with ipv4 or simple hostname @@ -125,6 +125,12 @@ def _matches_simple_hostname(host: str): return _simple_hostname_regex().fullmatch(host) if may_have_port and (host_seg := _port_validator(value)): + if host_seg.startswith("[") and host_seg.endswith("]"): + # ref: RFC 3986 (3.2.2), one pair of brackets encloses an IPv6 address + host_seg = host_seg[1:-1] + if skip_ipv6_addr or "[" in host_seg or "]" in host_seg: + return False + return ipv6(host_seg, cidr=False) return ( _matches_simple_hostname(host_seg) or domain(host_seg, consider_tld=consider_tld, rfc_1034=rfc_1034, rfc_2782=rfc_2782) diff --git a/tests/test_email.py b/tests/test_email.py index 9b82eb46..f31e43e0 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -72,3 +72,33 @@ def test_rejects_newline_after_local_part(local_part: str): def test_returns_failed_validation_on_idna_mapped_lookalike_domain(domain_part: str): """Test returns failed validation on email domains that only IDNA 2003 mapping makes valid.""" assert isinstance(email(f"user@{domain_part}"), ValidationError) + + +@pytest.mark.parametrize( + ("value", "ipv6_address", "ipv4_address"), + [ + ("user@[192.0.2.1]", False, True), + ("user@[2001:db8::1]", True, False), + ], +) +def test_returns_true_on_address_literal(value: str, ipv6_address: bool, ipv4_address: bool): + """Test returns true on an email whose domain is a bracketed IP address.""" + assert email(value, ipv6_address=ipv6_address, ipv4_address=ipv4_address) + + +@pytest.mark.parametrize( + "value", + [ + "user@[[192.0.2.1]]", + "user@[192.0.2.1]]", + "user@[[192.0.2.1]", + "user@[[2001:db8::1]]", + "user@[[[2001:db8::1]]]", + "user@[2001:db8::1]]", + "user@[2001:db8::1%]]", + "user@[]", + ], +) +def test_returns_failed_validation_on_address_literal_with_extra_brackets(value: str): + """Test returns failed validation on an address literal with more than one pair of brackets.""" + assert isinstance(email(value, ipv6_address=True, ipv4_address=True), ValidationError) diff --git a/tests/test_hostname.py b/tests/test_hostname.py index 83f00212..cd1932e2 100644 --- a/tests/test_hostname.py +++ b/tests/test_hostname.py @@ -171,3 +171,29 @@ def test_returns_true_on_valid_top_level_domain_hostname(value: str): def test_returns_failed_validation_on_invalid_top_level_domain_hostname(value: str): """Test returns failed validation on hostname with a TLD not allowed by IANA.""" assert isinstance(hostname(value, consider_tld=True), ValidationError) + + +@pytest.mark.parametrize( + "value", + [ + # ref: RFC 3986 (3.2.2), only an IPv6 address is enclosed in brackets + "[1.2.3.4]:80", + "[example.com]:80", + "[localhost]:80", + # exactly one pair of brackets + "[[::1]]:80", + "[[::1]:80", + "[::1]]:80", + "::1]:80", + "localhost]:80", + "[::1%]]:80", + ], +) +def test_returns_failed_validation_on_malformed_bracketed_host(value: str): + """Test returns failed validation on brackets that do not enclose one IPv6 address.""" + assert isinstance(hostname(value), ValidationError) + + +def test_returns_failed_validation_on_bracketed_ipv6_when_skipped(): + """Test returns failed validation on a bracketed IPv6 address when IPv6 is skipped.""" + assert isinstance(hostname("[::1]:80", skip_ipv6_addr=True), ValidationError)