]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
http1connection: Enforce max_body_size in _GzipMessageDelegate
authorBen Darnell <ben@bendarnell.com>
Tue, 26 May 2026 17:39:53 +0000 (13:39 -0400)
committerBen Darnell <ben@bendarnell.com>
Wed, 27 May 2026 16:08:33 +0000 (12:08 -0400)
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
tornado/test/httpserver_test.py

index a69b4dd6f4fd22b9e18929034e44295d7edc827b..17ada11f68036508265df266bfc5adf6244a2624 100644 (file)
@@ -179,7 +179,9 @@ class HTTP1Connection(httputil.HTTPConnection):
         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:
@@ -702,9 +704,16 @@ class HTTP1Connection(httputil.HTTPConnection):
 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: GzipDecompressor | None = None
 
     def headers_received(
@@ -729,6 +738,9 @@ class _GzipMessageDelegate(httputil.HTTPMessageDelegate):
                     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 0aebefb413528f2d85a8a3b49be486a8626b0f0e..02d4f4c0954d71cdeb888d1f3ac0e84487d8c8c4 100644 (file)
@@ -1105,7 +1105,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")
@@ -1126,6 +1126,10 @@ class GzipTest(GzipBaseTest, AsyncHTTPTestCase):
         )
         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):