From 7d77cb649bbbb46711d39da4075a2b0f95894fcd Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Wed, 3 Jun 2026 12:48:11 +0200 Subject: [PATCH] CVE-2026-44431 * Remove sensitive headers in proxy pools too * Add a changelog entry * Check retries history in tests Co-authored-by: Copilot --------- Co-authored-by: Copilot --- changelog/GHSA-qccp-gfcp-xxvc.bugfix.rst | 3 + src/urllib3/connectionpool.py | 12 ++++ .../test_proxy_poolmanager.py | 72 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 changelog/GHSA-qccp-gfcp-xxvc.bugfix.rst diff --git a/changelog/GHSA-qccp-gfcp-xxvc.bugfix.rst b/changelog/GHSA-qccp-gfcp-xxvc.bugfix.rst new file mode 100644 index 0000000..bac765e --- /dev/null +++ b/changelog/GHSA-qccp-gfcp-xxvc.bugfix.rst @@ -0,0 +1,3 @@ +Fixed HTTP pools created using ``ProxyManager.connection_from_url`` to strip +sensitive headers specified in ``Retry.remove_headers_on_redirect`` when +redirecting to a different host. diff --git a/src/urllib3/connectionpool.py b/src/urllib3/connectionpool.py index 402bf67..9466b32 100644 --- a/src/urllib3/connectionpool.py +++ b/src/urllib3/connectionpool.py @@ -852,6 +852,18 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): body = None headers = HTTPHeaderDict(headers)._prepare_for_method_change() + # Strip headers marked as unsafe to forward to the redirected location. + # Check remove_headers_on_redirect to avoid a potential network call within + # self.is_same_host() which may use socket.gethostbyname() in the future. + if retries.remove_headers_on_redirect and not self.is_same_host( + redirect_location + ): + new_headers = headers.copy() # type: ignore[union-attr] + for header in headers: + if header.lower() in retries.remove_headers_on_redirect: + new_headers.pop(header, None) + headers = new_headers + try: retries = retries.increment(method, url, response=response, _pool=self) except MaxRetryError: diff --git a/test/with_dummyserver/test_proxy_poolmanager.py b/test/with_dummyserver/test_proxy_poolmanager.py index 7b292a2..b8745fc 100644 --- a/test/with_dummyserver/test_proxy_poolmanager.py +++ b/test/with_dummyserver/test_proxy_poolmanager.py @@ -34,6 +34,7 @@ from urllib3.exceptions import ( SubjectAltNameWarning, ) from urllib3.poolmanager import ProxyManager, proxy_from_url +from urllib3.util.retry import RequestHistory from urllib3.util import Timeout from urllib3.util.ssl_ import create_urllib3_context @@ -277,6 +278,77 @@ class TestHTTPProxyManager(HTTPDummyProxyTestCase): ) assert r._pool.host != self.http_host_alt + _sensitive_headers = { + "Authorization": "foo", + "Proxy-Authorization": "bar", + "Cookie": "foo=bar", + } + + @pytest.mark.parametrize( + "sensitive_headers", + (_sensitive_headers, {k.lower(): v for k, v in _sensitive_headers.items()}), + ids=("capitalized", "lowercase"), + ) + def test_cross_host_redirect_remove_headers_via_proxy_manager( + self, sensitive_headers: dict[str, str] + ) -> None: + headers_url = f"{self.http_url_alt}/headers" + initial_url = f"{self.http_url}/redirect?target={headers_url}" + with proxy_from_url(self.proxy_url) as proxy_mgr: + r = proxy_mgr.request( + "GET", initial_url, headers=sensitive_headers, retries=1 + ) + assert r.status == 200 + assert r.retries is not None + assert r.retries.history == ( + RequestHistory( + method="GET", + url=initial_url, + error=None, + status=303, + redirect_location=headers_url, + ), + ) + data = r.json() + for header in sensitive_headers: + assert header not in data + + @pytest.mark.parametrize( + "sensitive_headers", + (_sensitive_headers, {k.lower(): v for k, v in _sensitive_headers.items()}), + ids=("capitalized", "lowercase"), + ) + def test_cross_host_redirect_remove_headers_via_pool( + self, sensitive_headers: dict[str, str] + ) -> None: + headers_url = f"{self.http_url_alt}/headers" + initial_url = f"{self.http_url}/redirect?target={headers_url}" + with proxy_from_url(self.proxy_url) as proxy_mgr: + pool = proxy_mgr.connection_from_url(self.http_url) + r = pool.urlopen( + "GET", + initial_url, + headers=sensitive_headers, + retries=1, + redirect=True, + assert_same_host=False, + preload_content=True, + ) + assert r.status == 200 + assert r.retries is not None + assert r.retries.history == ( + RequestHistory( + method="GET", + url=initial_url, + error=None, + status=303, + redirect_location=headers_url, + ), + ) + data = r.json() + for header in sensitive_headers: + assert header not in data + def test_cross_protocol_redirect(self): with proxy_from_url(self.proxy_url, ca_certs=DEFAULT_CA) as http: cross_protocol_location = "%s/echo?a=b" % self.https_url -- 2.54.0