diff --git a/Lib/http/client.py b/Lib/http/client.py index fe67d0f7963f786..0d7300e51e41a43 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -1010,11 +1010,15 @@ def _wrap_ipv6(self, ip): return ip def _tunnel(self): - if _contains_disallowed_url_pchar_re.search(self._tunnel_host): + # Check the host as it is sent: the IDNA codec normalizes the name + # first (NFKC), which maps some non-ASCII characters to ASCII ones, + # e.g. U+00A0 (NO-BREAK SPACE) to a space. + tunnel_host = self._tunnel_host.encode("idna") + if _contains_disallowed_url_pchar_re.search(tunnel_host.decode("ascii")): raise ValueError('Tunnel host can\'t contain control characters %r' % (self._tunnel_host,)) connect = b"CONNECT %s:%d %s\r\n" % ( - self._wrap_ipv6(self._tunnel_host.encode("idna")), + self._wrap_ipv6(tunnel_host), self._tunnel_port, self._http_vsn_str.encode("ascii")) headers = [connect] diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 1e5ec0d4e43c271..6979bc8edc7fdc4 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -405,6 +405,11 @@ def test_invalid_tunnel_host(self): 'invalid.host\r\n', 'invalid.host\x00', 'invalid host', + # The IDNA codec normalizes these to an ASCII space + 'invalid\xa0host', + 'invalid\u3000host', + # ... and this one to a space and a combining character + 'invalid\xa8host', ) for tunnel_host in cases: with self.subTest(tunnel_host): diff --git a/Misc/NEWS.d/next/Library/2026-10-22-05-23-04.gh-issue-161104.rvNPjJ.rst b/Misc/NEWS.d/next/Library/2026-10-22-05-23-04.gh-issue-161104.rvNPjJ.rst new file mode 100644 index 000000000000000..acae526043a2979 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-05-23-04.gh-issue-161104.rvNPjJ.rst @@ -0,0 +1,4 @@ +Fix :meth:`http.client.HTTPConnection.set_tunnel` accepting a tunnel host +containing a non-ASCII character that IDNA encoding maps to a space, such as +U+00A0 NO-BREAK SPACE. Connecting now raises :exc:`ValueError`, as it already +does for an ASCII space.