|
| 1 | +From 24d7b67eac89f94e11003424bcf0d8f7b72222a8 Mon Sep 17 00:00:00 2001 |
| 2 | +From: Illia Volochii <illia.volochii@gmail.com> |
| 3 | +Date: Fri, 5 Dec 2025 16:41:33 +0200 |
| 4 | +Subject: [PATCH] Merge commit from fork |
| 5 | + |
| 6 | +* Add a hard-coded limit for the decompression chain |
| 7 | + |
| 8 | +* Reuse new list |
| 9 | + |
| 10 | +Upstream Patch Reference: https://github.com/urllib3/urllib3/commit/24d7b67eac89f94e11003424bcf0d8f7b72222a8.patch |
| 11 | +--- |
| 12 | + changelog/GHSA-gm62-xv2j-4w53.security.rst | 4 ++++ |
| 13 | + src/urllib3/response.py | 13 ++++++++++++- |
| 14 | + test/test_response.py | 10 ++++++++++ |
| 15 | + 3 files changed, 26 insertions(+), 1 deletion(-) |
| 16 | + create mode 100644 changelog/GHSA-gm62-xv2j-4w53.security.rst |
| 17 | + |
| 18 | +diff --git a/changelog/GHSA-gm62-xv2j-4w53.security.rst b/changelog/GHSA-gm62-xv2j-4w53.security.rst |
| 19 | +new file mode 100644 |
| 20 | +index 0000000..6646eaa |
| 21 | +--- /dev/null |
| 22 | ++++ b/changelog/GHSA-gm62-xv2j-4w53.security.rst |
| 23 | +@@ -0,0 +1,4 @@ |
| 24 | ++Fixed a security issue where an attacker could compose an HTTP response with |
| 25 | ++virtually unlimited links in the ``Content-Encoding`` header, potentially |
| 26 | ++leading to a denial of service (DoS) attack by exhausting system resources |
| 27 | ++during decoding. The number of allowed chained encodings is now limited to 5. |
| 28 | +diff --git a/src/urllib3/response.py b/src/urllib3/response.py |
| 29 | +index 0bd13d4..0f8adbd 100644 |
| 30 | +--- a/src/urllib3/response.py |
| 31 | ++++ b/src/urllib3/response.py |
| 32 | +@@ -135,8 +135,19 @@ class MultiDecoder(object): |
| 33 | + they were applied. |
| 34 | + """ |
| 35 | + |
| 36 | ++ |
| 37 | ++ # Maximum allowed number of chained HTTP encodings in the |
| 38 | ++ # Content-Encoding header. |
| 39 | ++ max_decode_links = 5 |
| 40 | ++ |
| 41 | + def __init__(self, modes): |
| 42 | +- self._decoders = [_get_decoder(m.strip()) for m in modes.split(",")] |
| 43 | ++ encodings = [m.strip() for m in modes.split(",")] |
| 44 | ++ if len(encodings) > self.max_decode_links: |
| 45 | ++ raise DecodeError( |
| 46 | ++ "Too many content encodings in the chain: " |
| 47 | ++ f"{len(encodings)} > {self.max_decode_links}" |
| 48 | ++ ) |
| 49 | ++ self._decoders = [_get_decoder(e) for e in encodings] |
| 50 | + |
| 51 | + def flush(self): |
| 52 | + return self._decoders[0].flush() |
| 53 | +diff --git a/test/test_response.py b/test/test_response.py |
| 54 | +index e09e385..4bfa8af 100644 |
| 55 | +--- a/test/test_response.py |
| 56 | ++++ b/test/test_response.py |
| 57 | +@@ -295,6 +295,16 @@ class TestResponse(object): |
| 58 | + |
| 59 | + assert r.data == b"foo" |
| 60 | + |
| 61 | ++ def test_read_multi_decoding_too_many_links(self) -> None: |
| 62 | ++ fp = BytesIO(b"foo") |
| 63 | ++ with pytest.raises( |
| 64 | ++ DecodeError, match="Too many content encodings in the chain: 6 > 5" |
| 65 | ++ ): |
| 66 | ++ HTTPResponse( |
| 67 | ++ fp, |
| 68 | ++ headers={"content-encoding": "gzip, deflate, br, zstd, gzip, deflate"}, |
| 69 | ++ ) |
| 70 | ++ |
| 71 | + def test_body_blob(self): |
| 72 | + resp = HTTPResponse(b"foo") |
| 73 | + assert resp.data == b"foo" |
| 74 | +-- |
| 75 | +2.43.0 |
| 76 | + |
0 commit comments