Skip to content

Commit 8864ac4

Browse files
authored
Merge commit from fork
* Stop decoding response content during redirects needlessly * Rename the new query parameter * Add a changelog entry
1 parent 70cecb2 commit 8864ac4

4 files changed

Lines changed: 44 additions & 2 deletions

File tree

CHANGES.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
2.6.3 (TBD)
2+
==================
3+
4+
Bugfixes
5+
--------
6+
7+
- Fixed a high-severity security issue where decompression-bomb safeguards of
8+
the streaming API were bypassed when HTTP redirects were followed.
9+
(`GHSA-38jv-5279-wg99 <https://github.com/urllib3/urllib3/security/advisories/GHSA-38jv-5279-wg99>`__)
10+
11+
TODO: add other entries.
12+
13+
114
2.6.2 (2025-12-11)
215
==================
316

dummyserver/app.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,16 @@ async def redirect() -> ResponseReturnValue:
233233
values = await request.values
234234
target = values.get("target", "/")
235235
status = values.get("status", "303 See Other")
236+
compressed = values.get("compressed") == "true"
236237
status_code = status.split(" ")[0]
237238

238239
headers = [("Location", target)]
239-
return await make_response("", status_code, headers)
240+
if compressed:
241+
headers.append(("Content-Encoding", "gzip"))
242+
data = gzip.compress(b"foo")
243+
else:
244+
data = b""
245+
return await make_response(data, status_code, headers)
240246

241247

242248
@hypercorn_app.route("/redirect_after")

src/urllib3/response.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,11 @@ def drain_conn(self) -> None:
797797
Unread data in the HTTPResponse connection blocks the connection from being released back to the pool.
798798
"""
799799
try:
800-
self.read()
800+
self.read(
801+
# Do not spend resources decoding the content unless
802+
# decoding has already been initiated.
803+
decode_content=self._has_decoded_content,
804+
)
801805
except (HTTPError, OSError, BaseSSLError, HTTPException):
802806
pass
803807

test/with_dummyserver/test_connectionpool.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,25 @@ def test_redirect(self) -> None:
508508
assert r.status == 200
509509
assert r.data == b"Dummy server!"
510510

511+
@mock.patch("urllib3.response.GzipDecoder.decompress")
512+
def test_no_decoding_with_redirect_when_preload_disabled(
513+
self, gzip_decompress: mock.MagicMock
514+
) -> None:
515+
"""
516+
Test that urllib3 does not attempt to decode a gzipped redirect
517+
response when `preload_content` is set to `False`.
518+
"""
519+
with HTTPConnectionPool(self.host, self.port) as pool:
520+
# Three requests are expected: two redirects and one final / 200 OK.
521+
response = pool.request(
522+
"GET",
523+
"/redirect",
524+
fields={"target": "/redirect?compressed=true", "compressed": "true"},
525+
preload_content=False,
526+
)
527+
assert response.status == 200
528+
gzip_decompress.assert_not_called()
529+
511530
def test_303_redirect_makes_request_lose_body(self) -> None:
512531
with HTTPConnectionPool(self.host, self.port) as pool:
513532
response = pool.request(

0 commit comments

Comments
 (0)