From 5ec0de499b9166ca71c65ab04f2a7e4eb0d66fcc Mon Sep 17 00:00:00 2001
From: Illia Volochii <illia.volochii@gmail.com>
Date: Thu, 7 May 2026 18:40:31 +0300
Subject: [PATCH] Merge commit from fork

* Remove sensitive headers in proxy pools too

* Add a changelog entry

* Check retries history in tests

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
---
 dummyserver/asgi_proxy.py                     |  1 +
 src/urllib3/connectionpool.py                 | 12 ++++
 .../test_proxy_poolmanager.py                 | 72 +++++++++++++++++++
 4 files changed, 88 insertions(+)
 create mode 100644 changelog/GHSA-qccp-gfcp-xxvc.bugfix.rst

Index: urllib3-2.1.0/src/urllib3/connectionpool.py
===================================================================
--- urllib3-2.1.0.orig/src/urllib3/connectionpool.py
+++ urllib3-2.1.0/src/urllib3/connectionpool.py
@@ -899,6 +899,18 @@ class HTTPConnectionPool(ConnectionPool,
                 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:
Index: urllib3-2.1.0/test/with_dummyserver/test_proxy_poolmanager.py
===================================================================
--- urllib3-2.1.0.orig/test/with_dummyserver/test_proxy_poolmanager.py
+++ urllib3-2.1.0/test/with_dummyserver/test_proxy_poolmanager.py
@@ -33,6 +33,7 @@ from urllib3.exceptions import (
     SSLError,
 )
 from urllib3.poolmanager import ProxyManager, proxy_from_url
+from urllib3.util.retry import RequestHistory
 from urllib3.util.ssl_ import create_urllib3_context
 from urllib3.util.timeout import Timeout
 
@@ -248,6 +249,77 @@ class TestHTTPProxyManager(HTTPDummyProx
             assert r._pool is not None
             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) -> None:
         with proxy_from_url(self.proxy_url, ca_certs=DEFAULT_CA) as http:
             cross_protocol_location = f"{self.https_url}/echo?a=b"
