VYPR
High severityNVD Advisory· Published Oct 4, 2023· Updated Nov 3, 2025

`Cookie` HTTP header isn't stripped on cross-origin redirects

CVE-2023-43804

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
urllib3PyPI
>= 2.0.0, < 2.0.62.0.6
urllib3PyPI
< 1.26.171.26.17

Affected products

1

Patches

2
01220354d389

Backport GHSA-v845-jxx5-vc9f (#3139)

https://github.com/urllib3/urllib3Seth Michael LarsonOct 2, 2023via ghsa
5 files changed · +29 9
  • CHANGES.rst+6 0 modified
    @@ -1,6 +1,12 @@
     Changes
     =======
     
    +1.26.17 (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``.
    +
    +
     1.26.16 (2023-05-23)
     --------------------
     
    
  • src/urllib3/util/retry.py+1 1 modified
    @@ -235,7 +235,7 @@ class Retry(object):
         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"])
     
         #: Maximum backoff time.
         DEFAULT_BACKOFF_MAX = 120
    
  • test/test_retry_deprecated.py+1 1 modified
    @@ -295,7 +295,7 @@ def test_retry_method_not_in_whitelist(self):
         def test_retry_default_remove_headers_on_redirect(self):
             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):
             retry = Retry(remove_headers_on_redirect=["X-API-Secret"])
    
  • test/test_retry.py+2 2 modified
    @@ -293,12 +293,12 @@ def test_retry_method_not_in_whitelist(self):
         def test_retry_default_remove_headers_on_redirect(self):
             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):
             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", six.u("\xb2")])  # \xb2 = ^2
         def test_parse_retry_after_invalid(self, value):
    
  • test/with_dummyserver/test_poolmanager.py+19 5 modified
    @@ -141,20 +141,21 @@ def test_redirect_cross_host_remove_headers(self):
                     "GET",
                     "%s/redirect" % self.base_url,
                     fields={"target": "%s/headers" % self.base_url_alt},
    -                headers={"Authorization": "foo"},
    +                headers={"Authorization": "foo", "Cookie": "foo=bar"},
                 )
     
                 assert r.status == 200
     
                 data = json.loads(r.data.decode("utf-8"))
     
                 assert "Authorization" not in data
    +            assert "Cookie" not in data
     
                 r = http.request(
                     "GET",
                     "%s/redirect" % self.base_url,
                     fields={"target": "%s/headers" % self.base_url_alt},
    -                headers={"authorization": "foo"},
    +                headers={"authorization": "foo", "cookie": "foo=bar"},
                 )
     
                 assert r.status == 200
    @@ -163,14 +164,16 @@ def test_redirect_cross_host_remove_headers(self):
     
                 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):
             with PoolManager() as http:
                 r = http.request(
                     "GET",
                     "%s/redirect" % self.base_url,
                     fields={"target": "%s/headers" % self.base_url_alt},
    -                headers={"Authorization": "foo"},
    +                headers={"Authorization": "foo", "Cookie": "foo=bar"},
                     retries=Retry(remove_headers_on_redirect=[]),
                 )
     
    @@ -179,14 +182,19 @@ def test_redirect_cross_host_no_remove_headers(self):
                 data = json.loads(r.data.decode("utf-8"))
     
                 assert data["Authorization"] == "foo"
    +            assert data["Cookie"] == "foo=bar"
     
         def test_redirect_cross_host_set_removed_headers(self):
             with PoolManager() as http:
                 r = http.request(
                     "GET",
                     "%s/redirect" % self.base_url,
                     fields={"target": "%s/headers" % self.base_url_alt},
    -                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"]),
                 )
     
    @@ -196,12 +204,17 @@ def test_redirect_cross_host_set_removed_headers(self):
     
                 assert "X-API-Secret" not in data
                 assert data["Authorization"] == "bar"
    +            assert data["Cookie"] == "foo=bar"
     
                 r = http.request(
                     "GET",
                     "%s/redirect" % self.base_url,
                     fields={"target": "%s/headers" % self.base_url_alt},
    -                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"]),
                 )
     
    @@ -212,6 +225,7 @@ def test_redirect_cross_host_set_removed_headers(self):
                 assert "x-api-secret" not in data
                 assert "X-API-Secret" not in data
                 assert data["Authorization"] == "bar"
    +            assert data["Cookie"] == "foo=bar"
     
         def test_redirect_without_preload_releases_connection(self):
             with PoolManager(block=True, maxsize=2) as http:
    
644124ecd0b6

Merge pull request from GHSA-v845-jxx5-vc9f

https://github.com/urllib3/urllib3Quentin PradetOct 2, 2023via ghsa
5 files changed · +35 9
  • CHANGES.rst+5 0 modified
    @@ -1,3 +1,8 @@
    +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)
     ==================
     
    
  • docs/user-guide.rst+3 0 modified
    @@ -238,6 +238,9 @@ 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
    
  • src/urllib3/util/retry.py+1 1 modified
    @@ -187,7 +187,7 @@ class Retry:
         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
    
  • test/test_retry.py+2 2 modified
    @@ -334,12 +334,12 @@ def test_retry_method_not_allowed(self) -> None:
         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:
    
  • test/with_dummyserver/test_poolmanager.py+24 6 modified
    @@ -141,20 +141,21 @@ def test_redirect_cross_host_remove_headers(self) -> None:
                     "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
    @@ -163,14 +164,16 @@ def test_redirect_cross_host_remove_headers(self) -> None:
     
                 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=[]),
                 )
     
    @@ -179,14 +182,19 @@ def test_redirect_cross_host_no_remove_headers(self) -> None:
                 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"]),
                 )
     
    @@ -196,8 +204,13 @@ def test_redirect_cross_host_set_removed_headers(self) -> None:
     
                 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",
    @@ -213,9 +226,14 @@ def test_redirect_cross_host_set_removed_headers(self) -> None:
                 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:
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

16

News mentions

0

No linked articles in our index yet.