From 8760275afb72bd10b57d92cb4d52abf759b2f3a7 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 25 Oct 2024 06:46:50 -0700 Subject: [PATCH] apply max_form_memory_size another level up in the parser --- CHANGES.rst | 3 +++ src/werkzeug/formparser.py | 11 +++++++++++ src/werkzeug/sansio/multipart.py | 2 ++ tests/test_formparser.py | 12 ++++++++++++ 4 files changed, 28 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 879bbf9ac..f55d192c2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Version 3.0.6 Unreleased +- Fix how ``max_form_memory_size`` is applied when parsing large non-file + fields. :ghsa:`q34m-jh98-gwm2` + Version 3.0.5 ------------- diff --git a/src/werkzeug/formparser.py b/src/werkzeug/formparser.py index e4b1f27fd..3c6875e26 100644 --- a/src/werkzeug/formparser.py +++ b/src/werkzeug/formparser.py @@ -352,6 +352,7 @@ def parse( self, stream: t.IO[bytes], boundary: bytes, content_length: int | None ) -> tuple[MultiDict[str, str], MultiDict[str, FileStorage]]: current_part: Field | File + field_size: int | None = None container: t.IO[bytes] | list[bytes] _write: t.Callable[[bytes], t.Any] @@ -370,13 +371,23 @@ def parse( while not isinstance(event, (Epilogue, NeedData)): if isinstance(event, Field): current_part = event + field_size = 0 container = [] _write = container.append elif isinstance(event, File): current_part = event + field_size = None container = self.start_file_streaming(event, content_length) _write = container.write elif isinstance(event, Data): + if self.max_form_memory_size is not None and field_size is not None: + # Ensure that accumulated data events do not exceed limit. + # Also checked within single event in MultipartDecoder. + field_size += len(event.data) + + if field_size > self.max_form_memory_size: + raise RequestEntityTooLarge() + _write(event.data) if not event.more_data: if isinstance(current_part, Field): diff --git a/src/werkzeug/sansio/multipart.py b/src/werkzeug/sansio/multipart.py index fc8735378..731be0336 100644 --- a/src/werkzeug/sansio/multipart.py +++ b/src/werkzeug/sansio/multipart.py @@ -140,6 +140,8 @@ def receive_data(self, data: bytes | None) -> None: self.max_form_memory_size is not None and len(self.buffer) + len(data) > self.max_form_memory_size ): + # Ensure that data within single event does not exceed limit. + # Also checked across accumulated events in MultiPartParser. raise RequestEntityTooLarge() else: self.buffer.extend(data) diff --git a/tests/test_formparser.py b/tests/test_formparser.py index ed63be686..ebd7fddcf 100644 --- a/tests/test_formparser.py +++ b/tests/test_formparser.py @@ -456,3 +456,15 @@ def test_file_rfc2231_filename_continuations(self): ) as request: assert request.files["rfc2231"].filename == "a b c d e f.txt" assert request.files["rfc2231"].read() == b"file contents" + + +def test_multipart_max_form_memory_size() -> None: + """max_form_memory_size is tracked across multiple data events.""" + data = b"--bound\r\nContent-Disposition: form-field; name=a\r\n\r\n" + data += b"a" * 15 + b"\r\n--bound--" + # The buffer size is less than the max size, so multiple data events will be + # returned. The field size is greater than the max. + parser = formparser.MultiPartParser(max_form_memory_size=10, buffer_size=5) + + with pytest.raises(RequestEntityTooLarge): + parser.parse(io.BytesIO(data), b"bound", None)