Version in base suite: 1.26.12-1+deb12u1 Base version: python-urllib3_1.26.12-1+deb12u1 Target version: python-urllib3_1.26.12-1+deb12u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/python-urllib3/python-urllib3_1.26.12-1+deb12u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/python-urllib3/python-urllib3_1.26.12-1+deb12u2.dsc changelog | 12 ++ patches/CVE-2025-50181.patch | 202 +++++++++++++++++++++++++++++++++++++++++++ patches/CVE-2025-66418.patch | 65 +++++++++++++ patches/CVE-2026-21441.patch | 60 ++++++++++++ patches/series | 3 5 files changed, 342 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmplfb6swls/python-urllib3_1.26.12-1+deb12u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmplfb6swls/python-urllib3_1.26.12-1+deb12u2.dsc: no acceptable signature found diff -Nru python-urllib3-1.26.12/debian/changelog python-urllib3-1.26.12/debian/changelog --- python-urllib3-1.26.12/debian/changelog 2024-12-21 14:28:17.000000000 +0000 +++ python-urllib3-1.26.12/debian/changelog 2026-01-12 21:53:55.000000000 +0000 @@ -1,3 +1,15 @@ +python-urllib3 (1.26.12-1+deb12u2) bookworm-security; urgency=high + + * Non-maintainer upload by the Security Team. + * Redirects are not disabled when retries are disabled on PoolManager + instantiation (CVE-2025-50181) (Closes: #1108076) + * Unbounded number of links in the decompression chain (CVE-2025-66418) + (Closes: #1122030) + * Decompression-bomb safeguards bypassed when following HTTP redirects + (streaming API) (CVE-2026-21441) (Closes: #1125062) + + -- Salvatore Bonaccorso Mon, 12 Jan 2026 22:53:55 +0100 + python-urllib3 (1.26.12-1+deb12u1) bookworm; urgency=medium * Non-maintainer upload. diff -Nru python-urllib3-1.26.12/debian/patches/CVE-2025-50181.patch python-urllib3-1.26.12/debian/patches/CVE-2025-50181.patch --- python-urllib3-1.26.12/debian/patches/CVE-2025-50181.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-urllib3-1.26.12/debian/patches/CVE-2025-50181.patch 2026-01-03 22:00:03.000000000 +0000 @@ -0,0 +1,202 @@ +From: Illia Volochii +Date: Wed, 18 Jun 2025 16:25:01 +0300 +Subject: Merge commit from fork +Origin: https://github.com/urllib3/urllib3/commit/f05b1329126d5be6de501f9d1e3e36738bc08857 +Bug-Debian: https://bugs.debian.org/1108076 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-50181 + +* Apply Quentin's suggestion + +Co-authored-by: Quentin Pradet + +* Add tests for disabled redirects in the pool manager + +* Add a possible fix for the issue with not raised `MaxRetryError` + +* Make urllib3 handle redirects instead of JS when JSPI is used + +* Fix info in the new comment + +* State that redirects with XHR are not controlled by urllib3 + +* Remove excessive params from new test requests + +* Add tests reaching max non-0 redirects + +* Test redirects with Emscripten + +* Fix `test_merge_pool_kwargs` + +* Add a changelog entry + +* Parametrize tests + +* Drop a fix for Emscripten + +* Apply Seth's suggestion to docs + +Co-authored-by: Seth Michael Larson + +* Use a minor release instead of the patch one + +--------- + +Co-authored-by: Quentin Pradet +Co-authored-by: Seth Michael Larson +--- + CHANGES.rst | 9 ++ + docs/reference/contrib/emscripten.rst | 2 +- + dummyserver/app.py | 1 + + src/urllib3/poolmanager.py | 18 +++- + test/contrib/emscripten/test_emscripten.py | 16 ++++ + test/test_poolmanager.py | 5 +- + test/with_dummyserver/test_poolmanager.py | 101 +++++++++++++++++++++ + 7 files changed, 148 insertions(+), 4 deletions(-) + +--- a/src/urllib3/poolmanager.py ++++ b/src/urllib3/poolmanager.py +@@ -170,6 +170,22 @@ class PoolManager(RequestMethods): + + def __init__(self, num_pools=10, headers=None, **connection_pool_kw): + RequestMethods.__init__(self, headers) ++ if "retries" in connection_pool_kw: ++ retries = connection_pool_kw["retries"] ++ if not isinstance(retries, Retry): ++ # When Retry is initialized, raise_on_redirect is based ++ # on a redirect boolean value. ++ # But requests made via a pool manager always set ++ # redirect to False, and raise_on_redirect always ends ++ # up being False consequently. ++ # Here we fix the issue by setting raise_on_redirect to ++ # a value needed by the pool manager without considering ++ # the redirect boolean. ++ raise_on_redirect = retries is not False ++ retries = Retry.from_int(retries, redirect=False) ++ retries.raise_on_redirect = raise_on_redirect ++ connection_pool_kw = connection_pool_kw.copy() ++ connection_pool_kw["retries"] = retries + self.connection_pool_kw = connection_pool_kw + self.pools = RecentlyUsedContainer(num_pools, dispose_func=lambda p: p.close()) + +@@ -389,7 +405,7 @@ class PoolManager(RequestMethods): + kw["body"] = None + kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() + +- retries = kw.get("retries") ++ retries = kw.get("retries", response.retries) + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect) + +--- a/test/with_dummyserver/test_poolmanager.py ++++ b/test/with_dummyserver/test_poolmanager.py +@@ -82,6 +82,85 @@ class TestPoolManager(HTTPDummyServerTes + assert r.status == 200 + assert r.data == b"Dummy server!" + ++ @pytest.mark.parametrize( ++ "retries", ++ (0, Retry(total=0), Retry(redirect=0), Retry(total=0, redirect=0)), ++ ) ++ def test_redirects_disabled_for_pool_manager_with_0(self, retries): ++ """ ++ Check handling redirects when retries is set to 0 on the pool ++ manager. ++ """ ++ with PoolManager(retries=retries) as http: ++ with pytest.raises(MaxRetryError): ++ http.request("GET", f"{self.base_url}/redirect") ++ ++ # Setting redirect=True should not change the behavior. ++ with pytest.raises(MaxRetryError): ++ http.request("GET", f"{self.base_url}/redirect", redirect=True) ++ ++ # Setting redirect=False should not make it follow the redirect, ++ # but MaxRetryError should not be raised. ++ response = http.request("GET", f"{self.base_url}/redirect", redirect=False) ++ assert response.status == 303 ++ ++ @pytest.mark.parametrize( ++ "retries", ++ ( ++ False, ++ Retry(total=False), ++ Retry(redirect=False), ++ Retry(total=False, redirect=False), ++ ), ++ ) ++ def test_redirects_disabled_for_pool_manager_with_false(self, retries): ++ """ ++ Check that setting retries set to False on the pool manager disables ++ raising MaxRetryError and redirect=True does not change the ++ behavior. ++ """ ++ with PoolManager(retries=retries) as http: ++ response = http.request("GET", f"{self.base_url}/redirect") ++ assert response.status == 303 ++ ++ response = http.request("GET", f"{self.base_url}/redirect", redirect=True) ++ assert response.status == 303 ++ ++ response = http.request("GET", f"{self.base_url}/redirect", redirect=False) ++ assert response.status == 303 ++ ++ def test_redirects_disabled_for_individual_request(self): ++ """ ++ Check handling redirects when they are meant to be disabled ++ on the request level. ++ """ ++ with PoolManager() as http: ++ # Check when redirect is not passed. ++ with pytest.raises(MaxRetryError): ++ http.request("GET", f"{self.base_url}/redirect", retries=0) ++ response = http.request("GET", f"{self.base_url}/redirect", retries=False) ++ assert response.status == 303 ++ ++ # Check when redirect=True. ++ with pytest.raises(MaxRetryError): ++ http.request( ++ "GET", f"{self.base_url}/redirect", retries=0, redirect=True ++ ) ++ response = http.request( ++ "GET", f"{self.base_url}/redirect", retries=False, redirect=True ++ ) ++ assert response.status == 303 ++ ++ # Check when redirect=False. ++ response = http.request( ++ "GET", f"{self.base_url}/redirect", retries=0, redirect=False ++ ) ++ assert response.status == 303 ++ response = http.request( ++ "GET", f"{self.base_url}/redirect", retries=False, redirect=False ++ ) ++ assert response.status == 303 ++ + def test_cross_host_redirect(self): + with PoolManager() as http: + cross_host_location = "%s/echo?a=b" % self.base_url_alt +@@ -136,6 +215,24 @@ class TestPoolManager(HTTPDummyServerTes + pool = http.connection_from_host(self.host, self.port) + assert pool.num_connections == 1 + ++ # Check when retries are configured for the pool manager. ++ with PoolManager(retries=1) as http: ++ with pytest.raises(MaxRetryError): ++ http.request( ++ "GET", ++ f"{self.base_url}/redirect", ++ fields={"target": f"/redirect?target={self.base_url}/"}, ++ ) ++ ++ # Here we allow more retries for the request. ++ response = http.request( ++ "GET", ++ f"{self.base_url}/redirect", ++ fields={"target": f"/redirect?target={self.base_url}/"}, ++ retries=2, ++ ) ++ assert response.status == 200 ++ + def test_redirect_cross_host_remove_headers(self): + with PoolManager() as http: + r = http.request( diff -Nru python-urllib3-1.26.12/debian/patches/CVE-2025-66418.patch python-urllib3-1.26.12/debian/patches/CVE-2025-66418.patch --- python-urllib3-1.26.12/debian/patches/CVE-2025-66418.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-urllib3-1.26.12/debian/patches/CVE-2025-66418.patch 2026-01-03 22:10:37.000000000 +0000 @@ -0,0 +1,65 @@ +From: Illia Volochii +Date: Fri, 5 Dec 2025 16:41:33 +0200 +Subject: Merge commit from fork +Origin: https://github.com/urllib3/urllib3/commit/24d7b67eac89f94e11003424bcf0d8f7b72222a8 +Bug-Debian: https://bugs.debian.org/1122030 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-66418 + +* Add a hard-coded limit for the decompression chain + +* Reuse new list +--- + changelog/GHSA-gm62-xv2j-4w53.security.rst | 4 ++++ + src/urllib3/response.py | 12 +++++++++++- + test/test_response.py | 10 ++++++++++ + 3 files changed, 25 insertions(+), 1 deletion(-) + create mode 100644 changelog/GHSA-gm62-xv2j-4w53.security.rst + +--- /dev/null ++++ b/changelog/GHSA-gm62-xv2j-4w53.security.rst +@@ -0,0 +1,4 @@ ++Fixed a security issue where an attacker could compose an HTTP response with ++virtually unlimited links in the ``Content-Encoding`` header, potentially ++leading to a denial of service (DoS) attack by exhausting system resources ++during decoding. The number of allowed chained encodings is now limited to 5. +--- a/src/urllib3/response.py ++++ b/src/urllib3/response.py +@@ -134,8 +134,18 @@ class MultiDecoder(object): + they were applied. + """ + ++ # Maximum allowed number of chained HTTP encodings in the ++ # Content-Encoding header. ++ max_decode_links = 5 ++ + def __init__(self, modes): +- self._decoders = [_get_decoder(m.strip()) for m in modes.split(",")] ++ encodings = [m.strip() for m in modes.split(",")] ++ if len(encodings) > self.max_decode_links: ++ raise DecodeError( ++ "Too many content encodings in the chain: " ++ f"{len(encodings)} > {self.max_decode_links}" ++ ) ++ self._decoders = [_get_decoder(e) for e in encodings] + + def flush(self): + return self._decoders[0].flush() +--- a/test/test_response.py ++++ b/test/test_response.py +@@ -286,6 +286,16 @@ class TestResponse(object): + + assert r.data == b"foo" + ++ def test_read_multi_decoding_too_many_links(self): ++ fp = BytesIO(b"foo") ++ with pytest.raises( ++ DecodeError, match="Too many content encodings in the chain: 6 > 5" ++ ): ++ HTTPResponse( ++ fp, ++ headers={"content-encoding": "gzip, deflate, br, zstd, gzip, deflate"}, ++ ) ++ + def test_body_blob(self): + resp = HTTPResponse(b"foo") + assert resp.data == b"foo" diff -Nru python-urllib3-1.26.12/debian/patches/CVE-2026-21441.patch python-urllib3-1.26.12/debian/patches/CVE-2026-21441.patch --- python-urllib3-1.26.12/debian/patches/CVE-2026-21441.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-urllib3-1.26.12/debian/patches/CVE-2026-21441.patch 2026-01-12 21:53:55.000000000 +0000 @@ -0,0 +1,60 @@ +From: Illia Volochii +Date: Wed, 7 Jan 2026 18:07:30 +0200 +Subject: Merge commit from fork +Origin: https://github.com/urllib3/urllib3/commit/8864ac407bba8607950025e0979c4c69bc7abc7b +Bug-Debian: https://bugs.debian.org/1125062 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-21441 + +* Stop decoding response content during redirects needlessly + +* Rename the new query parameter + +* Add a changelog entry +--- + CHANGES.rst | 13 +++++++++++++ + dummyserver/app.py | 8 +++++++- + src/urllib3/response.py | 6 +++++- + test/with_dummyserver/test_connectionpool.py | 19 +++++++++++++++++++ + 4 files changed, 44 insertions(+), 2 deletions(-) + +--- a/src/urllib3/response.py ++++ b/src/urllib3/response.py +@@ -301,7 +301,11 @@ class HTTPResponse(io.IOBase): + Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. + """ + try: +- self.read() ++ self.read( ++ # Do not spend resources decoding the content unless ++ # decoding has already been initiated. ++ decode_content=self._has_decoded_content, ++ ) + except (HTTPError, SocketError, BaseSSLError, HTTPException): + pass + +--- a/test/with_dummyserver/test_connectionpool.py ++++ b/test/with_dummyserver/test_connectionpool.py +@@ -411,6 +411,23 @@ class TestConnectionPool(HTTPDummyServer + assert r.status == 200 + assert r.data == b"Dummy server!" + ++ @mock.patch("urllib3.response.GzipDecoder.decompress") ++ def test_no_decoding_with_redirect_when_preload_disabled(self, gzip_decompress): ++ """ ++ Test that urllib3 does not attempt to decode a gzipped redirect ++ response when `preload_content` is set to `False`. ++ """ ++ with HTTPConnectionPool(self.host, self.port) as pool: ++ # Three requests are expected: two redirects and one final / 200 OK. ++ response = pool.request( ++ "GET", ++ "/redirect", ++ fields={"target": "/redirect?compressed=true", "compressed": "true"}, ++ preload_content=False, ++ ) ++ assert response.status == 200 ++ gzip_decompress.assert_not_called() ++ + def test_303_redirect_makes_request_lose_body(self): + with HTTPConnectionPool(self.host, self.port) as pool: + response = pool.request( diff -Nru python-urllib3-1.26.12/debian/patches/series python-urllib3-1.26.12/debian/patches/series --- python-urllib3-1.26.12/debian/patches/series 2024-12-21 14:28:17.000000000 +0000 +++ python-urllib3-1.26.12/debian/patches/series 2026-01-12 21:53:55.000000000 +0000 @@ -3,3 +3,6 @@ CVE-2023-43804.patch CVE-2023-45803.patch CVE-2024-37891.patch +CVE-2025-50181.patch +CVE-2025-66418.patch +CVE-2026-21441.patch