CVE-2023-43804: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: urllib3 is a user-friendly HTTP client library for Python. urllib3 doesn't treat the `Cookie` HTTP header special or provide any helpers for managing cookies over HTTP, that is the responsibility of the user. However, it is possible for a user to specify a `Cookie` header and unknowingly leak information via HTTP redirects to a different origin if that user doesn't disable redirects explicitly. This issue has been patched in urllib3 version 1.26.17 or 2.0.5.
CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 8.1
Published: 2023-10-04T17:15Z
Repository: https://github.com/urllib3/urllib3
Language: Python
Files changed: CHANGES.rst, docs/user-guide.rst, src/urllib3/util/retry.py, test/test_retry.py, test/with_dummyserver/test_poolmanager.py
Commit date: 2023-10-02 10:46:16 -0500
Commit message: Merge pull request from GHSA-v845-jxx5-vc9f


Diff: diff --git a/CHANGES.rst b/CHANGES.rst
index c2ecdcae..e551cbea 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,10 @@
+2.0.6 (2023-10-02)
+==================
+
+* Added the ``Cookie`` header to the list of headers to strip from requests when redirecting to a different host. As before, different headers can be set via ``Retry.remove_headers_on_redirect``.
+
 2.0.5 (2023-09-20)
 ==================
 
 - Allowed pyOpenSSL third-party module without any deprecation warning. (`#3126 <https://github.com/urllib3/urllib3/issues/3126>`__)
 - Fixed default ``blocksize`` of ``HTTPConnection`` classes to match high-level classes. Previously was 8KiB, now 16KiB. (`#3066 <https://github.com/urllib3/urllib3/issues/3066>`__)
diff --git a/docs/user-guide.rst b/docs/user-guide.rst
index 5cf9f50d..9416fe12 100644
--- a/docs/user-guide.rst
+++ b/docs/user-guide.rst
@@ -236,10 +236,13 @@ the ``;`` delimited key-value pairs:
     )
 
     print(resp.json())
     # {"cookies": {"id": "30", "session": "f3efe9db"}}  
 
+Note that the ``Cookie`` header will be stripped if the server redirects to a
+different host.
+
 Cookies provided by the server are stored in the ``Set-Cookie`` header:
 
 .. code-block:: python
 
     import urllib3
diff --git a/src/urllib3/util/retry.py b/src/urllib3/util/retry.py
index ea48afe3..7572bfd2 100644
--- a/src/urllib3/util/retry.py
+++ b/src/urllib3/util/retry.py
@@ -185,11 +185,11 @@ class Retry:
 
     #: Default status codes to be used for ``status_forcelist``
     RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503])
 
     #: Default headers to be used for ``remove_headers_on_redirect``
-    DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(["Authorization"])
+    DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(["Cookie", "Authorization"])
 
     #: Default maximum backoff time.
     DEFAULT_BACKOFF_MAX = 120
 
     # Backward compatibility; assigned outside of the class.
diff --git a/test/test_retry.py b/test/test_retry.py
index 4fb2bba1..f71e7acc 100644
--- a/test/test_retry.py
+++ b/test/test_retry.py
@@ -332,16 +332,16 @@ class TestRetry:
             retry.increment(method="POST", error=error)
 
     def test_retry_default_remove_headers_on_redirect(self) -> None:
         retry = Retry()
 
-        assert list(retry.remove_headers_on_redirect) == ["authorization"]
+        assert retry.remove_headers_on_redirect == {"authorization", "cookie"}
 
     def test_retry_set_remove_headers_on_redirect(self) -> None:
         retry = Retry(remove_headers_on_redirect=["X-API-Secret"])
 
-        assert list(retry.remove_headers_on_redirect) == ["x-api-secret"]
+        assert retry.remove_headers_on_redirect == {"x-api-secret"}
 
     @pytest.mark.parametrize("value", ["-1", "+1", "1.0", "\xb2"])  # \xb2 = ^2
     def test_parse_retry_after_invalid(self, value: str) -> None:
         retry = Retry()
         with pytest.raises(InvalidHeader):
diff --git a/test/with_dummyserver/test_poolmanager.py b/test/with_dummyserver/test_poolmanager.py
index e4c55071..da802a38 100644
--- a/test/with_dummyserver/test_poolmanager.py
+++ b/test/with_dummyserver/test_poolmanager.py
@@ -139,67 +139,80 @@ class TestPoolManager(HTTPDummyServerTestCase):
         with PoolManager() as http:
             r = http.request(
                 "GET",
                 f"{self.base_url}/redirect",
                 fields={"target": f"{self.base_url_alt}/headers"},
-                headers={"Authorization": "foo"},
+                headers={"Authorization": "foo", "Cookie": "foo=bar"},
             )
 
             assert r.status == 200
 
             data = r.json()
 
             assert "Authorization" not in data
+            assert "Cookie" not in data
 
             r = http.request(
                 "GET",
                 f"{self.base_url}/redirect",
                 fields={"target": f"{self.base_url_alt}/headers"},
-                headers={"authorization": "foo"},
+                headers={"authorization": "foo", "cookie": "foo=bar"},
             )
 
             assert r.status == 200
 
             data = r.json()
 
             assert "authorization" not in data
             assert "Authorization" not in data
+            assert "cookie" not in data
+            assert "Cookie" not in data
 
     def test_redirect_cross_host_no_remove_headers(self) -> None:
         with PoolManager() as http:
             r = http.request(
                 "GET",
                 f"{self.base_url}/redirect",
                 fields={"target": f"{self.base_url_alt}/headers"},
-                headers={"Authorization": "foo"},
+                headers={"Authorization": "foo", "Cookie": "foo=bar"},
                 retries=Retry(remove_headers_on_redirect=[]),
             )
 
             assert r.status == 200
 
             data = r.json()
 
             assert data["Authorization"] == "foo"
+            assert data["Cookie"] == "foo=bar"
 
     def test_redirect_cross_host_set_removed_headers(self) -> None:
         with PoolManager() as http:
             r = http.request(
                 "GET",
                 f"{self.base_url}/redirect",
                 fields={"target": f"{self.base_url_alt}/headers"},
-                headers={"X-API-Secret": "foo", "Authorization": "bar"},
+                headers={
+                    "X-API-Secret": "foo",
+                    "Authorization": "bar",
+                    "Cookie": "foo=bar",
+                },
                 retries=Retry(remove_headers_on_redirect=["X-API-Secret"]),
             )
 
             assert r.status == 200
 
             data = r.json()
 
             assert "X-API-Secret" not in data
             assert data["Authorization"] == "bar"
+            assert data["Cookie"] == "foo=bar"
 
-            headers = {"x-api-secret": "foo", "authorization": "bar"}
+            headers = {
+                "x-api-secret": "foo",
+                "authorization": "bar",
+                "cookie": "foo=bar",
+            }
             r = http.request(
                 "GET",
                 f"{self.base_url}/redirect",
                 fields={"target": f"{self.base_url_alt}/headers"},
                 headers=headers,
@@ -211,13 +224,18 @@ class TestPoolManager(HTTPDummyServerTestCase):
             data = r.json()
 
             assert "x-api-secret" not in data
             assert "X-API-Secret" not in data
             assert data["Authorization"] == "bar"
+            assert data["Cookie"] == "foo=bar"
 
             # Ensure the header argument itself is not modified in-place.
-            assert headers == {"x-api-secret": "foo", "authorization": "bar"}
+            assert headers == {
+                "x-api-secret": "foo",
+                "authorization": "bar",
+                "cookie": "foo=bar",
+            }
 
     def test_redirect_without_preload_releases_connection(self) -> None:
         with PoolManager(block=True, maxsize=2) as http:
             r = http.request("GET", f"{self.base_url}/redirect", preload_content=False)
             assert isinstance(r, HTTPResponse)

Vulnerable code:     DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(["Authorization"])
        assert list(retry.remove_headers_on_redirect) == ["authorization"]
        assert list(retry.remove_headers_on_redirect) == ["x-api-secret"]
                headers={"Authorization": "foo"},
                headers={"authorization": "foo"},
                headers={"Authorization": "foo"},
                headers={"X-API-Secret": "foo", "Authorization": "bar"},
            headers = {"x-api-secret": "foo", "authorization": "bar"}
            assert headers == {"x-api-secret": "foo", "authorization": "bar"}
Fixed code: 2.0.6 (2023-10-02)
==================

* Added the ``Cookie`` header to the list of headers to strip from requests when redirecting to a different host. As before, different headers can be set via ``Retry.remove_headers_on_redirect``.

Note that the ``Cookie`` header will be stripped if the server redirects to a
different host.

    DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(["Cookie", "Authorization"])
        assert retry.remove_headers_on_redirect == {"authorization", "cookie"}
        assert retry.remove_headers_on_redirect == {"x-api-secret"}
                headers={"Authorization": "foo", "Cookie": "foo=bar"},
            assert "Cookie" not in data
                headers={"authorization": "foo", "cookie": "foo=bar"},
            assert "cookie" not in data
            assert "Cookie" not in data
                headers={"Authorization": "foo", "Cookie": "foo=bar"},
            assert data["Cookie"] == "foo=bar"
                headers={
                    "X-API-Secret": "foo",
                    "Authorization": "bar",
                    "Cookie": "foo=bar",
                },
            assert data["Cookie"] == "foo=bar"
            headers = {
                "x-api-secret": "foo",
                "authorization": "bar",
                "cookie": "foo=bar",
            }
            assert data["Cookie"] == "foo=bar"
            assert headers == {
                "x-api-secret": "foo",
                "authorization": "bar",
                "cookie": "foo=bar",
            }