VYPR
Moderate severityNVD Advisory· Published Oct 25, 2024· Updated Jan 3, 2025

Werkzeug possible resource exhaustion when parsing file data in forms

CVE-2024-49767

Description

Werkzeug is a Web Server Gateway Interface web application library. Applications using werkzeug.formparser.MultiPartParser corresponding to a version of Werkzeug prior to 3.0.6 to parse multipart/form-data requests (e.g. all flask applications) are vulnerable to a relatively simple but effective resource exhaustion (denial of service) attack. A specifically crafted form submission request can cause the parser to allocate and block 3 to 8 times the upload size in main memory. There is no upper limit; a single upload at 1 Gbit/s can exhaust 32 GB of RAM in less than 60 seconds. Werkzeug version 3.0.6 fixes this issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
WerkzeugPyPI
< 3.0.63.0.6
QuartPyPI
< 0.20.00.20.0

Affected products

1

Patches

3
abb04a512496

Support max_form_parts and max_form_memory_size

https://github.com/pallets/quartpgjonesDec 23, 2024via ghsa
5 files changed · +97 27
  • src/quart/app.py+5 2 modified
    @@ -248,6 +248,8 @@ class Quart(App):
                 "EXPLAIN_TEMPLATE_LOADING": False,
                 "MAX_CONTENT_LENGTH": 16 * 1024 * 1024,  # 16 MB Limit
                 "MAX_COOKIE_SIZE": 4093,
    +            "MAX_FORM_MEMORY_SIZE": 500_000,
    +            "MAX_FORM_PARTS": 1_000,
                 "PERMANENT_SESSION_LIFETIME": timedelta(days=31),
                 # Replaces PREFERRED_URL_SCHEME to allow for WebSocket scheme
                 "PREFER_SECURE_URLS": False,
    @@ -1130,8 +1132,9 @@ async def handle_websocket_exception(
     
         def log_exception(
             self,
    -        exception_info: tuple[type, BaseException, TracebackType]
    -        | tuple[None, None, None],
    +        exception_info: (
    +            tuple[type, BaseException, TracebackType] | tuple[None, None, None]
    +        ),
         ) -> None:
             """Log a exception to the :attr:`logger`.
     
    
  • src/quart/formparser.py+35 16 modified
    @@ -43,15 +43,20 @@ class FormDataParser:
     
         def __init__(
             self,
    -        stream_factory: StreamFactory = default_stream_factory,
    -        max_form_memory_size: int | None = None,
    -        max_content_length: int | None = None,
    +        *,
             cls: type[MultiDict] | None = MultiDict,
    +        max_content_length: int | None = None,
    +        max_form_memory_size: int | None = None,
    +        max_form_parts: int | None = None,
             silent: bool = True,
    +        stream_factory: StreamFactory = default_stream_factory,
         ) -> None:
    -        self.stream_factory = stream_factory
             self.cls = cls
    +        self.max_content_length = max_content_length
    +        self.max_form_memory_size = max_form_memory_size
    +        self.max_form_parts = max_form_parts
             self.silent = silent
    +        self.stream_factory = stream_factory
     
         def get_parse_func(
             self, mimetype: str, options: dict[str, str]
    @@ -87,9 +92,12 @@ async def _parse_multipart(
             options: dict[str, str],
         ) -> tuple[MultiDict, MultiDict]:
             parser = MultiPartParser(
    -            self.stream_factory,
                 cls=self.cls,
                 file_storage_cls=self.file_storage_class,
    +            max_content_length=self.max_content_length,
    +            max_form_memory_size=self.max_form_memory_size,
    +            max_form_parts=self.max_form_parts,
    +            stream_factory=self.stream_factory,
             )
             boundary = options.get("boundary", "").encode("ascii")
     
    @@ -105,10 +113,14 @@ async def _parse_urlencoded(
             content_length: int | None,
             options: dict[str, str],
         ) -> tuple[MultiDict, MultiDict]:
    -        form = parse_qsl(
    -            (await body).decode(),
    -            keep_blank_values=True,
    -        )
    +        try:
    +            form = parse_qsl(
    +                (await body).decode(),
    +                keep_blank_values=True,
    +                max_num_fields=self.max_form_parts,
    +            )
    +        except ValueError:
    +            raise RequestEntityTooLarge() from None
             return self.cls(form), self.cls()
     
         parse_functions: dict[str, ParserFunc] = {
    @@ -121,17 +133,22 @@ async def _parse_urlencoded(
     class MultiPartParser:
         def __init__(
             self,
    -        stream_factory: StreamFactory = default_stream_factory,
    -        max_form_memory_size: int | None = None,
    -        cls: type[MultiDict] = MultiDict,
    +        *,
             buffer_size: int = 64 * 1024,
    +        cls: type[MultiDict] = MultiDict,
             file_storage_cls: type[FileStorage] = FileStorage,
    +        max_content_length: int | None = None,
    +        max_form_memory_size: int | None = None,
    +        max_form_parts: int | None = None,
    +        stream_factory: StreamFactory = default_stream_factory,
         ) -> None:
    -        self.max_form_memory_size = max_form_memory_size
    -        self.stream_factory = stream_factory
    -        self.cls = cls
             self.buffer_size = buffer_size
    +        self.cls = cls
             self.file_storage_cls = file_storage_cls
    +        self.max_content_length = max_content_length
    +        self.max_form_memory_size = max_form_memory_size
    +        self.max_form_parts = max_form_parts
    +        self.stream_factory = stream_factory
     
         def fail(self, message: str) -> NoReturn:
             raise ValueError(message)
    @@ -172,7 +189,9 @@ async def parse(
             container: IO[bytes] | list[bytes]
             _write: Callable[[bytes], Any]
     
    -        parser = MultipartDecoder(boundary, self.max_form_memory_size)
    +        parser = MultipartDecoder(
    +            boundary, self.max_content_length, max_parts=self.max_form_parts
    +        )
     
             fields = []
             files = []
    
  • src/quart/wrappers/base.py+0 9 modified
    @@ -8,7 +8,6 @@
     from werkzeug.sansio.request import Request as SansIORequest
     
     from .. import json
    -from ..globals import current_app
     
     if TYPE_CHECKING:
         from ..routing import QuartRule  # noqa
    @@ -73,14 +72,6 @@ def __init__(
             self.http_version = http_version
             self.scope = scope
     
    -    @property
    -    def max_content_length(self) -> int | None:
    -        """Read-only view of the ``MAX_CONTENT_LENGTH`` config key."""
    -        if current_app:
    -            return current_app.config["MAX_CONTENT_LENGTH"]
    -        else:
    -            return None
    -
         @property
         def endpoint(self) -> str | None:
             """Returns the corresponding endpoint matched for this request.
    
  • src/quart/wrappers/request.py+47 0 modified
    @@ -141,6 +141,9 @@ class Request(BaseRequestWebsocket):
         body_class = Body
         form_data_parser_class = FormDataParser
         lock_class = asyncio.Lock
    +    _max_content_length: int | None = None
    +    _max_form_memory_size: int | None = None
    +    _max_form_parts: int | None = None
     
         def __init__(
             self,
    @@ -189,6 +192,48 @@ def __init__(
             self._parsing_lock = self.lock_class()
             self._send_push_promise = send_push_promise
     
    +    @property
    +    def max_content_length(self) -> int | None:
    +        if self._max_content_length is not None:
    +            return self._max_content_length
    +
    +        if current_app:
    +            return current_app.config["MAX_CONTENT_LENGTH"]
    +
    +        return None
    +
    +    @max_content_length.setter
    +    def max_content_length(self, value: int | None) -> None:
    +        self._max_content_length = value
    +
    +    @property
    +    def max_form_memory_size(self) -> int | None:
    +        if self._max_form_memory_size is not None:
    +            return self._max_form_memory_size
    +
    +        if current_app:
    +            return current_app.config["MAX_FORM_MEMORY_SIZE"]
    +
    +        return None
    +
    +    @max_form_memory_size.setter
    +    def max_form_memory_size(self, value: int | None) -> None:
    +        self._max_form_memory_size = value
    +
    +    @property
    +    def max_form_parts(self) -> int | None:
    +        if self._max_form_parts is not None:
    +            return self._max_form_parts
    +
    +        if current_app:
    +            return current_app.config["MAX_FORM_PARTS"]
    +
    +        return None
    +
    +    @max_form_parts.setter
    +    def max_form_parts(self, value: int | None) -> None:
    +        self._max_form_parts = value
    +
         @property
         async def stream(self) -> NoReturn:
             raise NotImplementedError("Use body instead")
    @@ -284,6 +329,8 @@ async def files(self) -> MultiDict:
         def make_form_data_parser(self) -> FormDataParser:
             return self.form_data_parser_class(
                 max_content_length=self.max_content_length,
    +            max_form_memory_size=self.max_form_memory_size,
    +            max_form_parts=self.max_form_parts,
                 cls=self.parameter_storage_class,
             )
     
    
  • tests/test_formparser.py+10 0 modified
    @@ -3,6 +3,7 @@
     import pytest
     from werkzeug.exceptions import RequestEntityTooLarge
     
    +from quart.formparser import FormDataParser
     from quart.formparser import MultiPartParser
     from quart.wrappers.request import Body
     
    @@ -19,3 +20,12 @@ async def test_multipart_max_form_memory_size() -> None:
     
         with pytest.raises(RequestEntityTooLarge):
             await parser.parse(body, b"bound", 0)
    +
    +
    +async def test_formparser_max_num_parts() -> None:
    +    parser = FormDataParser(max_form_parts=1)
    +    body = Body(None, None)
    +    body.set_result(b"param1=data1&param2=data2&param3=data3")
    +
    +    with pytest.raises(RequestEntityTooLarge):
    +        await parser.parse(body, "application/x-url-encoded", None)
    
50cfeebcb072

Merge commit from fork

https://github.com/pallets/werkzeugDavid LordOct 25, 2024via ghsa
4 files changed · +28 0
  • CHANGES.rst+3 0 modified
    @@ -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
     -------------
    
  • src/werkzeug/formparser.py+11 0 modified
    @@ -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):
    
  • src/werkzeug/sansio/multipart.py+2 0 modified
    @@ -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)
    
  • tests/test_formparser.py+12 0 modified
    @@ -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)
    
5e78c4169b8e

apply max_form_memory_size another level up in the parser

https://github.com/pallets/quartDavid LordOct 25, 2024via ghsa
4 files changed · +38 1
  • CHANGES.rst+6 0 modified
    @@ -1,3 +1,9 @@
    +0.19.7
    +------
    +
    +* Security Fix how ``max_form_memory_size`` is applied when parsing large
    +  non-file fields. https://github.com/advisories/GHSA-q34m-jh98-gwm2
    +
     0.19.6 2024-05-19
     -----------------
     
    
  • pyproject.toml+1 1 modified
    @@ -1,6 +1,6 @@
     [tool.poetry]
     name = "Quart"
    -version = "0.19.6"
    +version = "0.19.7.dev"
     description = "A Python ASGI web microframework with the same API as Flask"
     authors = ["pgjones <philip.graham.jones@googlemail.com>"]
     classifiers = [
    
  • src/quart/formparser.py+10 0 modified
    @@ -15,6 +15,7 @@
     from urllib.parse import parse_qsl
     
     from werkzeug.datastructures import Headers, MultiDict
    +from werkzeug.exceptions import RequestEntityTooLarge
     from werkzeug.formparser import default_stream_factory
     from werkzeug.http import parse_options_header
     from werkzeug.sansio.multipart import Data, Epilogue, Field, File, MultipartDecoder, NeedData
    @@ -173,19 +174,28 @@ async def parse(
             files = []
     
             current_part: Field | File
    +        field_size: int | None = None
             async for data in body:
                 parser.receive_data(data)
                 event = parser.next_event()
                 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 field_size is not None:
    +                        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):
    
  • tests/test_formparser.py+21 0 added
    @@ -0,0 +1,21 @@
    +from __future__ import annotations
    +
    +import pytest
    +from werkzeug.exceptions import RequestEntityTooLarge
    +
    +from quart.formparser import MultiPartParser
    +from quart.wrappers.request import Body
    +
    +
    +async 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--"
    +    body = Body(None, None)
    +    body.set_result(data)
    +    # 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 = MultiPartParser(max_form_memory_size=10, buffer_size=5)
    +
    +    with pytest.raises(RequestEntityTooLarge):
    +        await parser.parse(body, b"bound", 0)
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.