`Cookie` HTTP header isn't stripped on cross-origin redirects
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.
| Package | Affected versions | Patched versions |
|---|---|---|
urllib3PyPI | >= 2.0.0, < 2.0.6 | 2.0.6 |
urllib3PyPI | < 1.26.17 | 1.26.17 |
Affected products
1Patches
201220354d389Backport GHSA-v845-jxx5-vc9f (#3139)
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:
644124ecd0b6Merge pull request from GHSA-v845-jxx5-vc9f
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- github.com/advisories/GHSA-v845-jxx5-vc9fghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-43804ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/urllib3/PYSEC-2023-192.yamlghsaWEB
- github.com/urllib3/urllib3/commit/01220354d389cd05474713f8c982d05c9b17aafbghsax_refsource_MISCWEB
- github.com/urllib3/urllib3/commit/644124ecd0b6e417c527191f866daa05a5a2056dghsax_refsource_MISCWEB
- github.com/urllib3/urllib3/security/advisories/GHSA-v845-jxx5-vc9fghsax_refsource_CONFIRMWEB
- lists.debian.org/debian-lts-announce/2023/10/msg00012.htmlghsaWEB
- lists.debian.org/debian-lts-announce/2024/12/msg00020.htmlghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5F5CUBAN5XMEBVBZPHFITBLMJV5FIJJ5ghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/I3PR7C6RJ6JUBQKIJ644DMIJSUP36VDYghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/NDAGZXYJ7H2G3SB47M453VQVNAWKAEJJghsaWEB
- security.netapp.com/advisory/ntap-20241213-0007ghsaWEB
- www.vicarius.io/vsociety/posts/cve-2023-43804-urllib3-vulnerability-3ghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5F5CUBAN5XMEBVBZPHFITBLMJV5FIJJ5/mitre
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/I3PR7C6RJ6JUBQKIJ644DMIJSUP36VDY/mitre
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/NDAGZXYJ7H2G3SB47M453VQVNAWKAEJJ/mitre
News mentions
0No linked articles in our index yet.