From ff808b33adc52d89a549376a5e3628e92abbc8ff Mon Sep 17 00:00:00 2001
From: Ben Darnell <ben@bendarnell.com>
Date: Tue, 26 May 2026 13:39:53 -0400
Subject: [PATCH] http1connection: Enforce max_body_size in
 _GzipMessageDelegate

This ensures we limit the post-decompression size of the body, and not
only the compressed size (which is enforced via the Content-Length
header at header-processing time).
---
 tornado/http1connection.py      | 16 ++++++++++++++--
 tornado/test/httpserver_test.py |  6 +++++-
 2 files changed, 19 insertions(+), 3 deletions(-)

Index: tornado-6.4/tornado/http1connection.py
===================================================================
--- tornado-6.4.orig/tornado/http1connection.py
+++ tornado-6.4/tornado/http1connection.py
@@ -64,6 +64,9 @@ class _ExceptionLoggingContext(object):
     ) -> None:
         if value is not None:
             assert typ is not None
+            # Let HTTPInputError pass through to higher-level handler
+            if isinstance(value, httputil.HTTPInputError):
+                return None
             self.logger.error("Uncaught exception", exc_info=(typ, value, tb))
             raise _QuietException
 
@@ -177,7 +180,9 @@ class HTTP1Connection(httputil.HTTPConne
         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:
@@ -711,9 +716,16 @@ class HTTP1Connection(httputil.HTTPConne
 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(
@@ -738,6 +750,9 @@ class _GzipMessageDelegate(httputil.HTTP
                     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
Index: tornado-6.4/tornado/test/httpserver_test.py
===================================================================
--- tornado-6.4.orig/tornado/test/httpserver_test.py
+++ tornado-6.4/tornado/test/httpserver_test.py
@@ -1034,7 +1034,7 @@ class GzipBaseTest(AsyncHTTPTestCase):
 
 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")
@@ -1055,6 +1055,10 @@ class GzipTest(GzipBaseTest, AsyncHTTPTe
         )
         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):
Index: tornado-6.4/tornado/test/httpclient_test.py
===================================================================
--- tornado-6.4.orig/tornado/test/httpclient_test.py
+++ tornado-6.4/tornado/test/httpclient_test.py
@@ -461,7 +461,7 @@ Transfer-Encoding: chunked
         # test if client hangs on tricky invalid gzip
         # curl/simple httpclient have different behavior (exception, logging)
         with ExpectLog(
-            app_log, "(Uncaught exception|Exception in callback)", required=False
+            gen_log, ".*Malformed HTTP message.*unconsumed gzip data", required=False
         ):
             try:
                 response = self.fetch("/invalid_gzip")
