tornado AsyncHTTPClient accumulates decompressed chunks without size limit (gzip bomb)
Description
Tornado's gzip decompression in SimpleAsyncHTTPClient lacks a cumulative size limit, allowing a malicious server to exhaust memory via a gzip bomb; fixed in 6.5.6.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Tornado's gzip decompression in SimpleAsyncHTTPClient lacks a cumulative size limit, allowing a malicious server to exhaust memory via a gzip bomb; fixed in 6.5.6.
Vulnerability
Tornado's gzip decompression routines in SimpleAsyncHTTPClient (default configuration) process data in chunks but do not enforce an overall limit on the total decompressed size, only on the compressed size. This allows a malicious server to send a small compressed payload that decompresses to a very large size (gzip bomb). Affected versions are prior to Tornado 6.5.6. HTTPServer is not affected unless decompress_request=True is set. [1][2]
Exploitation
An attacker controlling a server that the client connects to can send a response with a small compressed body that decompresses to an extremely large size. The client's SimpleAsyncHTTPClient will accumulate the decompressed chunks in memory without a cumulative limit. No authentication or special privileges are required; the attacker only needs to serve a malicious response. [1][2]
Impact
Successful exploitation leads to uncontrolled memory consumption on the client side, potentially causing denial of service (DoS) due to memory exhaustion. The attacker does not gain code execution or data access, but can disrupt the client's operation. [1][2]
Mitigation
The fix is in Tornado 6.5.6, where max_body_size is now checked against both the compressed and cumulative decompressed size. Prior to upgrading, users can set decompress_response=False or switch to CurlAsyncHTTPClient as a workaround. [1][2]
AI Insight generated on Jun 15, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
3- Range: < 6.5.6
- ghsa-coords2 versions
< 6.5.6+ 1 more
- (no CPE)range: < 6.5.6
- (no CPE)range: < 6.5.7-1.1
Patches
1ff808b33adc5http1connection: Enforce max_body_size in _GzipMessageDelegate
2 files changed · +19 −3
tornado/http1connection.py+14 −2 modified@@ -182,7 +182,9 @@ def read_response(self, delegate: httputil.HTTPMessageDelegate) -> Awaitable[boo been read. The result is true if the stream is still open. """ if self.params.decompress: - delegate = _GzipMessageDelegate(delegate, self.params.chunk_size) + delegate = _GzipMessageDelegate( + delegate, self.params.chunk_size, self._max_body_size + ) return self._read_message(delegate) async def _read_message(self, delegate: httputil.HTTPMessageDelegate) -> bool: @@ -705,9 +707,16 @@ async def _read_body_until_close( class _GzipMessageDelegate(httputil.HTTPMessageDelegate): """Wraps an `HTTPMessageDelegate` to decode ``Content-Encoding: gzip``.""" - def __init__(self, delegate: httputil.HTTPMessageDelegate, chunk_size: int) -> None: + def __init__( + self, + delegate: httputil.HTTPMessageDelegate, + chunk_size: int, + max_body_size: int, + ) -> None: self._delegate = delegate self._chunk_size = chunk_size + self._max_body_size = max_body_size + self._decompressed_body_size = 0 self._decompressor = None # type: Optional[GzipDecompressor] def headers_received( @@ -732,6 +741,9 @@ async def data_received(self, chunk: bytes) -> None: compressed_data, self._chunk_size ) if decompressed: + self._decompressed_body_size += len(decompressed) + if self._decompressed_body_size > self._max_body_size: + raise httputil.HTTPInputError("decompressed body too large") ret = self._delegate.data_received(decompressed) if ret is not None: await ret
tornado/test/httpserver_test.py+5 −1 modified@@ -1133,7 +1133,7 @@ def test_uncompressed(self): class GzipTest(GzipBaseTest, AsyncHTTPTestCase): def get_httpserver_options(self): - return dict(decompress_request=True) + return dict(decompress_request=True, max_body_size=100) def test_gzip(self): response = self.post_gzip("foo=bar") @@ -1154,6 +1154,10 @@ def test_gzip_case_insensitive(self): ) self.assertEqual(json_decode(response.body), {"foo": ["bar"]}) + def test_size_limit(self): + with ExpectLog(gen_log, ".*decompressed body too large", level=logging.INFO): + self.post_gzip("x" * 101) + class GzipUnsupportedTest(GzipBaseTest, AsyncHTTPTestCase): def test_gzip_unsupported(self):
Vulnerability mechanics
Root cause
"Missing cumulative size limit on decompressed gzip data allows unbounded memory allocation."
Attack vector
A malicious HTTP server sends a small gzip-compressed response that decompresses to a very large payload (a decompression bomb). When `SimpleAsyncHTTPClient` (default configuration) or `HTTPServer` with `decompress_request=True` processes the response, the `_GzipMessageDelegate` decompresses each chunk but never enforces an upper bound on the total decompressed size [CWE-409]. The attacker does not need authentication; they only need to serve a crafted response to a client that uses gzip decompression.
Affected code
The vulnerability is in `tornado/http1connection.py` in the `_GzipMessageDelegate` class. The `data_received` method decompresses gzip chunks without tracking the cumulative decompressed size, allowing unlimited memory consumption. The patch adds `_decompressed_body_size` tracking and a check against `max_body_size`.
What the fix does
The patch adds a `_decompressed_body_size` counter and a `_max_body_size` parameter to `_GzipMessageDelegate`. In `data_received`, after each decompression step the cumulative size is compared against `max_body_size`; if exceeded, an `HTTPInputError` is raised. This ensures the limit applies to the post-decompression size, not just the compressed Content-Length. The test `test_size_limit` verifies that a 101-byte uncompressed payload triggers the error.
Preconditions
- configThe client must use SimpleAsyncHTTPClient (default) or HTTPServer with decompress_request=True.
- networkThe attacker controls an HTTP server that sends a gzip-compressed response with a high compression ratio.
Generated on Jun 15, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.