python3.12-urllib3/SOURCES/CVE-2026-44432.patch
2026-06-29 08:47:49 -04:00

74 lines
2.8 KiB
Diff

From 7f655554452d03b459c589db3459380a8579c10c Mon Sep 17 00:00:00 2001
From: Lumir Balhar <lbalhar@redhat.com>
Date: Wed, 3 Jun 2026 12:01:02 +0200
Subject: [PATCH] CVE-2026-44432: drain_conn must not decompress after partial
read
---
src/urllib3/response.py | 12 +++++++-----
test/test_response.py | 25 +++++++++++++++++++++++++
2 files changed, 32 insertions(+), 5 deletions(-)
diff --git a/src/urllib3/response.py b/src/urllib3/response.py
index 1357d65..c27947b 100644
--- a/src/urllib3/response.py
+++ b/src/urllib3/response.py
@@ -480,11 +480,13 @@ class HTTPResponse(io.IOBase):
Unread data in the HTTPResponse connection blocks the connection from being released back to the pool.
"""
try:
- self.read(
- # Do not spend resources decoding the content unless
- # decoding has already been initiated.
- decode_content=self._has_decoded_content,
- )
+ # Discard any decoder state so that we read and discard raw bytes
+ # without decompression, avoiding decompression-bomb resource
+ # exhaustion (CVE-2026-44432).
+ self._decoder = None
+ self._has_decoded_content = False
+ self._decoded_buffer = BytesQueueBuffer()
+ self.read(decode_content=False)
except (HTTPError, SocketError, BaseSSLError, HTTPException):
pass
diff --git a/test/test_response.py b/test/test_response.py
index 597ce46..7b7c13d 100644
--- a/test/test_response.py
+++ b/test/test_response.py
@@ -745,6 +745,31 @@ class TestResponse(object):
resp.read(1, decode_content=False)
assert resp.read(1, decode_content=True) == b"o"
+ @mock.patch("urllib3.response.DeflateDecoder.decompress")
+ def test_drain_conn_after_partial_read_does_not_decompress(
+ self, deflate_decompress
+ ):
+ # CVE-2026-44432: drain_conn after a partial decoded read must not
+ # decompress the remainder of the body (decompression-bomb risk).
+ compress = zlib.compressobj(6, zlib.DEFLATED, -zlib.MAX_WBITS)
+ data = compress.compress(b"foobar")
+ data += compress.flush()
+
+ fp = BytesIO(data)
+ resp = HTTPResponse(
+ fp, headers={"content-encoding": "deflate"}, preload_content=False
+ )
+
+ # Partially read with decoding; _has_decoded_content becomes True.
+ deflate_decompress.return_value = b"f"
+ assert resp.read(1) == b"f"
+ assert resp._has_decoded_content
+
+ # drain_conn must not call the decoder (no decompression of remainder).
+ deflate_decompress.reset_mock()
+ resp.drain_conn()
+ deflate_decompress.assert_not_called()
+
def test_streaming(self):
fp = BytesIO(b"foo")
resp = HTTPResponse(fp, preload_content=False)
--
2.54.0