diff --git a/Lib/http/cookiejar.py b/Lib/http/cookiejar.py index 68cf16c93cc1c8..77786b0113a116 100644 --- a/Lib/http/cookiejar.py +++ b/Lib/http/cookiejar.py @@ -532,15 +532,41 @@ def parse_ns_headers(ns_headers): return result +# only kept for backwards compatibilty. IPV4_RE = re.compile(r"\.\d+$", re.ASCII) + +def _is_ipv4_hostname(text): + from ipaddress import IPv4Address + try: + IPv4Address(text) + except ValueError: + return False + return True + +def _is_ipv6_hostname(text): + if text.startswith('[') and text.endswith(']'): + from ipaddress import IPv6Address + try: + IPv6Address(text[1:-1]) + except ValueError: + return False + return True + return False + +def is_ip_like_hostname(text): + """Check if *text* is a valid IP-like hostname. + + A valid IP-like hostname is either an IPv4 address or + an IPv6 enclosed in brackets (for instance, "[::1]"). + """ + return _is_ipv4_hostname(text) or _is_ipv6_hostname(text) + def is_HDN(text): """Return True if text is a host domain name.""" # XXX # This may well be wrong. Which RFC is HDN defined in, if any (for # the purposes of RFC 2965)? - # For the current implementation, what about IPv6? Remember to look - # at other uses of IPV4_RE also, if change this. - if IPV4_RE.search(text): + if is_ip_like_hostname(text): return False if text == "": return False @@ -593,9 +619,7 @@ def liberal_is_HDN(text): For accepting/blocking domains. """ - if IPV4_RE.search(text): - return False - return True + return not is_ip_like_hostname(text) def user_domain_match(A, B): """For blocking/accepting domains. @@ -641,7 +665,10 @@ def eff_request_host(request): """ erhn = req_host = request_host(request) - if "." not in req_host: + if "." not in req_host and not _is_ipv6_hostname(req_host): + # Avoid adding .local at the end of an IPv6 address. + # See RFC 2965 [1] for the rationale of ".local". + # [1]: https://www.rfc-editor.org/rfc/rfc2965 erhn = req_host + ".local" return req_host, erhn diff --git a/Lib/test/test_http_cookiejar.py b/Lib/test/test_http_cookiejar.py index 04cb440cd4ccf6..5a051b47699bd7 100644 --- a/Lib/test/test_http_cookiejar.py +++ b/Lib/test/test_http_cookiejar.py @@ -16,7 +16,7 @@ CookieJar, DefaultCookiePolicy, LWPCookieJar, MozillaCookieJar, LoadError, lwp_cookie_str, DEFAULT_HTTP_PORT, escape_path, reach, is_HDN, domain_match, user_domain_match, request_path, - request_port, request_host) + request_port, request_host, is_ip_like_hostname) mswindows = (sys.platform == "win32") @@ -860,12 +860,25 @@ def test_is_HDN(self): self.assertTrue(is_HDN("foo.bar.com")) self.assertTrue(is_HDN("1foo2.3bar4.5com")) self.assertFalse(is_HDN("192.168.1.1")) + self.assertFalse(is_HDN("[::1]")) + self.assertFalse(is_HDN("[2001:db8:85a3::8a2e:370:7334]")) self.assertFalse(is_HDN("")) self.assertFalse(is_HDN(".")) self.assertFalse(is_HDN(".foo.bar.com")) self.assertFalse(is_HDN("..foo")) self.assertFalse(is_HDN("foo.")) + def test_is_ip_like_hostname(self): + self.assertTrue(is_ip_like_hostname('[::1]')) + self.assertTrue(is_ip_like_hostname('[2001:db8:85a3::8a2e:370:7334]')) + self.assertTrue(is_ip_like_hostname('192.168.0.1')) + + self.assertFalse(is_ip_like_hostname('::1')) + self.assertFalse(is_ip_like_hostname('256.256.256.256')) + self.assertFalse(is_ip_like_hostname('[::2001:db8:85a3::]')) + self.assertFalse(is_ip_like_hostname('acme.com')) + self.assertFalse(is_ip_like_hostname('')) + def test_reach(self): self.assertEqual(reach("www.acme.com"), ".acme.com") self.assertEqual(reach("acme.com"), "acme.com") @@ -875,9 +888,16 @@ def test_reach(self): self.assertEqual(reach("."), ".") self.assertEqual(reach(""), "") self.assertEqual(reach("192.168.0.1"), "192.168.0.1") + self.assertEqual(reach("[::1]"), "[::1]") + self.assertEqual(reach("[2001:db8:85a3::8a2e:370:7334]"), + "[2001:db8:85a3::8a2e:370:7334]") def test_domain_match(self): self.assertTrue(domain_match("192.168.1.1", "192.168.1.1")) + self.assertTrue(domain_match("[::1]", "[::1]")) + self.assertFalse(domain_match("[::1]", "::1")) + self.assertTrue(domain_match("[2001:db8:85a3::8a2e:370:7334]", + "[2001:db8:85a3::8a2e:370:7334]")) self.assertFalse(domain_match("192.168.1.1", ".168.1.1")) self.assertTrue(domain_match("x.y.com", "x.Y.com")) self.assertTrue(domain_match("x.y.com", ".Y.com")) @@ -905,8 +925,11 @@ def test_domain_match(self): self.assertFalse(user_domain_match("x.y.com", ".m")) self.assertFalse(user_domain_match("x.y.com", "")) self.assertFalse(user_domain_match("x.y.com", ".")) - self.assertTrue(user_domain_match("192.168.1.1", "192.168.1.1")) # not both HDNs, so must string-compare equal to match + self.assertTrue(user_domain_match("192.168.1.1", "192.168.1.1")) + self.assertTrue(user_domain_match("[::1]", "[::1]")) + self.assertTrue(domain_match("2001:db8::", "2001:db8::")) + self.assertFalse(user_domain_match("[::1]", "::1")) self.assertFalse(user_domain_match("192.168.1.1", ".168.1.1")) self.assertFalse(user_domain_match("192.168.1.1", ".")) # empty string is a special case @@ -1168,6 +1191,38 @@ def test_domain_block(self): c.add_cookie_header(req) self.assertFalse(req.has_header("Cookie")) + def test_block_ip_domains(self): + pol = DefaultCookiePolicy( + rfc2965=True, blocked_domains=[]) + c = CookieJar(policy=pol) + headers = ["Set-Cookie: CUSTOMER=WILE_E_COYOTE; path=/"] + + pol.set_blocked_domains(["[::1]"]) + req = urllib.request.Request("http://[::1]:8080") + res = FakeResponse(headers, "http://[::1]:8080") + c.extract_cookies(res, req) + self.assertEqual(len(c), 0) + + pol.set_blocked_domains(["[2001:db8:85a3::8a2e:370:7334]"]) + req = urllib.request.Request("http://[2001:db8:85a3::8a2e:370:7334]:8080") + res = FakeResponse(headers, "http://[2001:db8:85a3::8a2e:370:7334]:8080") + c.extract_cookies(res, req) + self.assertEqual(len(c), 0) + + pol.set_blocked_domains([""]) + req = urllib.request.Request("http://[::1]:8080") + res = FakeResponse(headers, "http://[::1]:8080") + c.extract_cookies(res, req) + self.assertEqual(len(c), 1) + + c.clear() + + pol.set_blocked_domains(["::1"]) + req = urllib.request.Request("http://[::1]:8080") + res = FakeResponse(headers, "http://[::1]:8080") + c.extract_cookies(res, req) + self.assertEqual(len(c), 1) + def test_secure(self): for ns in True, False: for whitespace in " ", "": diff --git a/Misc/NEWS.d/next/Library/2025-06-20-17-03-51.gh-issue-135768.DhUJWf.rst b/Misc/NEWS.d/next/Library/2025-06-20-17-03-51.gh-issue-135768.DhUJWf.rst new file mode 100644 index 00000000000000..a6a2b5a3285362 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-20-17-03-51.gh-issue-135768.DhUJWf.rst @@ -0,0 +1,2 @@ +:mod:`http.cookiejar`: fix allowed and blocked IPv6 domains +in :class:`~http.cookiejar.DefaultCookiePolicy`.