74 lines
2.8 KiB
Diff
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
|
|
|