urllib3/urllib3 · error · ValueError

Tunnel host can't contain control characters %r

Error message

Tunnel host can't contain control characters %r

What it means

In the Python<3.11.16 backport of HTTPConnection._tunnel (src/urllib3/connection.py:297), urllib3 rejects a tunnel host containing control/whitespace characters (matched by _contains_disallowed_url_pchar_re = [\x00-\x20\x7f]) before sending the CONNECT request. This prevents request smuggling / header injection through a crafted proxy host. ValueError is raised at line 299.

Source

Thrown at src/urllib3/connection.py:299

            if b":" in ip and ip[0] != b"["[0]:
                return b"[" + ip + b"]"
            return ip

        # Copied from CPython 3.12.13 Lib/http/client.py
        _is_legal_header_name = staticmethod(re.compile(rb"[^:\s][^:\r\n]*").fullmatch)
        _is_illegal_header_value = staticmethod(
            re.compile(rb"\n(?![ \t])|\r(?![ \t\n])").search
        )
        _contains_disallowed_url_pchar_re = re.compile("[\x00-\x20\x7f]")

        if sys.version_info < (3, 11, 16):
            # `_tunnel` copied from 3.11.15 backporting
            # https://github.com/python/cpython/commit/0d4026432591d43185568dd31cef6a034c4b9261
            # and https://github.com/python/cpython/commit/6fbc61070fda2ffb8889e77e3b24bca4249ab4d1
            # plus a fix from https://github.com/python/cpython/commit/56b7100b04e44ea27989242b176beb8f016b2c53
            def _tunnel(self) -> None:
                if self._contains_disallowed_url_pchar_re.search(self._tunnel_host):  # type: ignore[arg-type]
                    raise ValueError(
                        "Tunnel host can't contain control characters %r"
                        % (self._tunnel_host,)
                    )
                _MAXLINE = http.client._MAXLINE  # type: ignore[attr-defined]
                connect = b"CONNECT %s:%d HTTP/1.0\r\n" % (  # type: ignore[str-format]
                    self._wrap_ipv6(self._tunnel_host.encode("ascii")),  # type: ignore[union-attr]
                    self._tunnel_port,
                )
                headers = [connect]
                for header, value in self._tunnel_headers.items():  # type: ignore[attr-defined]
                    header_bytes = header.encode("latin-1")
                    value_bytes = value.encode("latin-1")
                    if not self._is_legal_header_name(header_bytes):
                        raise ValueError(f"Invalid header name {header_bytes!r}")
                    if self._is_illegal_header_value(value_bytes):
                        raise ValueError(f"Invalid header value {value_bytes!r}")
                    headers.append(b"%s: %s\r\n" % (header_bytes, value_bytes))
                headers.append(b"\r\n")

View on GitHub (pinned to c8d039c1b7)

Solutions

  1. Sanitize the proxy host: strip and reject any char in \x00-\x20 or \x7f before configuring the tunnel.
  2. Parse proxy URLs with urllib3.util.url.parse_url and validate the host is a clean hostname/IP.
  3. Source proxy config from trusted, immutable config rather than raw env vars.
  4. Reject leading/trailing whitespace and any embedded \r\n in host values.

Example fix

# before
os.environ["HTTPS_PROXY"] = "http://evil .host:8080\r\nX-Inject: yes"  # rejected at tunnel

# after
import re
_clean = re.compile(r"[\x00-\x20\x7f]")
host = _clean.sub("", proxy_host)
assert host == proxy_host, "proxy host has control chars"
conn.set_tunnel(host, port)
Defensive patterns

Strategy: validation

Validate before calling

import re
_BAD_HOST = re.compile(r"[\x00-\x20\x7f]")
def clean_proxy_host(host: str) -> str:
    if _BAD_HOST.search(host):
        raise ValueError(f"proxy host has control chars: {host!r}")
    return host

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Tunneling through a proxy whose configured host contains spaces, tabs, newlines, NUL, DEL, or other control bytes; e.g. a proxy URL parsed from untrusted input, a host with a trailing CRLF, or an env var carrying injected characters. Only affects interpreters below 3.11.16 (the guard is a vendored security backport).

Common situations: ALL_PROXY/HTTPS_PROXY env vars sourced from untrusted data; config files with stray whitespace/newlines; a host string built by string concatenation that included a CRLF; test fixtures with malformed hosts.

Related errors


AI-assisted analysis of urllib3/urllib3@c8d039c1b7 (2026-08-04). Data as JSON: /data/errors/fd4149e6c5a45ade.json. Report an issue: GitHub.