From 7f655554452d03b459c589db3459380a8579c10c Mon Sep 17 00:00:00 2001 From: Lumir Balhar 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