{"id":"fd4149e6c5a45ade","repo":"urllib3/urllib3","slug":"tunnel-host-can-t-contain-control-characters-r","errorCode":null,"errorMessage":"Tunnel host can't contain control characters %r","messagePattern":"Tunnel host can't contain control characters %r","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/urllib3/connection.py","lineNumber":299,"sourceCode":"            if b\":\" in ip and ip[0] != b\"[\"[0]:\n                return b\"[\" + ip + b\"]\"\n            return ip\n\n        # Copied from CPython 3.12.13 Lib/http/client.py\n        _is_legal_header_name = staticmethod(re.compile(rb\"[^:\\s][^:\\r\\n]*\").fullmatch)\n        _is_illegal_header_value = staticmethod(\n            re.compile(rb\"\\n(?![ \\t])|\\r(?![ \\t\\n])\").search\n        )\n        _contains_disallowed_url_pchar_re = re.compile(\"[\\x00-\\x20\\x7f]\")\n\n        if sys.version_info < (3, 11, 16):\n            # `_tunnel` copied from 3.11.15 backporting\n            # https://github.com/python/cpython/commit/0d4026432591d43185568dd31cef6a034c4b9261\n            # and https://github.com/python/cpython/commit/6fbc61070fda2ffb8889e77e3b24bca4249ab4d1\n            # plus a fix from https://github.com/python/cpython/commit/56b7100b04e44ea27989242b176beb8f016b2c53\n            def _tunnel(self) -> None:\n                if self._contains_disallowed_url_pchar_re.search(self._tunnel_host):  # type: ignore[arg-type]\n                    raise ValueError(\n                        \"Tunnel host can't contain control characters %r\"\n                        % (self._tunnel_host,)\n                    )\n                _MAXLINE = http.client._MAXLINE  # type: ignore[attr-defined]\n                connect = b\"CONNECT %s:%d HTTP/1.0\\r\\n\" % (  # type: ignore[str-format]\n                    self._wrap_ipv6(self._tunnel_host.encode(\"ascii\")),  # type: ignore[union-attr]\n                    self._tunnel_port,\n                )\n                headers = [connect]\n                for header, value in self._tunnel_headers.items():  # type: ignore[attr-defined]\n                    header_bytes = header.encode(\"latin-1\")\n                    value_bytes = value.encode(\"latin-1\")\n                    if not self._is_legal_header_name(header_bytes):\n                        raise ValueError(f\"Invalid header name {header_bytes!r}\")\n                    if self._is_illegal_header_value(value_bytes):\n                        raise ValueError(f\"Invalid header value {value_bytes!r}\")\n                    headers.append(b\"%s: %s\\r\\n\" % (header_bytes, value_bytes))\n                headers.append(b\"\\r\\n\")","sourceCodeStart":281,"sourceCodeEnd":317,"githubUrl":"https://github.com/urllib3/urllib3/blob/c8d039c1b743f0bf5ee136972c68350fd91d41f1/src/urllib3/connection.py#L281-L317","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Sanitize the proxy host: strip and reject any char in \\x00-\\x20 or \\x7f before configuring the tunnel.","Parse proxy URLs with urllib3.util.url.parse_url and validate the host is a clean hostname/IP.","Source proxy config from trusted, immutable config rather than raw env vars.","Reject leading/trailing whitespace and any embedded \\r\\n in host values."],"exampleFix":"# before\nos.environ[\"HTTPS_PROXY\"] = \"http://evil .host:8080\\r\\nX-Inject: yes\"  # rejected at tunnel\n\n# after\nimport re\n_clean = re.compile(r\"[\\x00-\\x20\\x7f]\")\nhost = _clean.sub(\"\", proxy_host)\nassert host == proxy_host, \"proxy host has control chars\"\nconn.set_tunnel(host, port)","handlingStrategy":"validation","validationCode":"import re\n_BAD_HOST = re.compile(r\"[\\x00-\\x20\\x7f]\")\ndef clean_proxy_host(host: str) -> str:\n    if _BAD_HOST.search(host):\n        raise ValueError(f\"proxy host has control chars: {host!r}\")\n    return host","typeGuard":"null","tryCatchPattern":"null","preventionTips":["Reject any control/whitespace char in proxy host config.","Parse proxy URLs with urllib3.util.url.parse_url and validate.","Treat proxy env vars as untrusted; sanitize before use.","Prefer fixed, reviewed proxy config over runtime env vars."],"tags":["security","proxy","tunnel","header-injection","validation"],"analyzedSha":"c8d039c1b743f0bf5ee136972c68350fd91d41f1","analyzedAt":"2026-08-04T20:12:33.219Z","schemaVersion":2}