VYPR
High severityNVD Advisory· Published Feb 14, 2023· Updated Mar 10, 2025

Werkzeug may allow high resource usage when parsing multipart form data with many fields

CVE-2023-25577

Description

Werkzeug is a comprehensive WSGI web application library. Prior to version 2.2.3, Werkzeug's multipart form data parser will parse an unlimited number of parts, including file parts. Parts can be a small amount of bytes, but each requires CPU time to parse and may use more memory as Python data. If a request can be made to an endpoint that accesses request.data, request.form, request.files, or request.get_data(parse_form_data=False), it can cause unexpectedly high resource usage. This allows an attacker to cause a denial of service by sending crafted multipart data to an endpoint that will parse it. The amount of CPU time required can block worker processes from handling legitimate requests. The amount of RAM required can trigger an out of memory kill of the process. Unlimited file parts can use up memory and file handles. If many concurrent requests are sent continuously, this can exhaust or kill all available workers. Version 2.2.3 contains a patch for this issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
WerkzeugPyPI
< 2.2.32.2.3

Affected products

1

Patches

1
517cac5a804e

Merge pull request from GHSA-xg9f-g7g7-2323

https://github.com/pallets/werkzeugDavid LordFeb 14, 2023via ghsa
6 files changed · +60 18
  • CHANGES.rst+4 0 modified
    @@ -21,6 +21,10 @@ Unreleased
         the requested size in one ``read`` call. :issue:`2558`
     -   A cookie header that starts with ``=`` is treated as an empty key and discarded,
         rather than stripping the leading ``==``.
    +-   Specify a maximum number of multipart parts, default 1000, after which a
    +    ``RequestEntityTooLarge`` exception is raised on parsing. This mitigates a DoS
    +    attack where a larger number of form/file parts would result in disproportionate
    +    resource use.
     
     
     Version 2.2.2
    
  • docs/request_data.rst+20 17 modified
    @@ -73,23 +73,26 @@ read the stream *or* call :meth:`~Request.get_data`.
     Limiting Request Data
     ---------------------
     
    -To avoid being the victim of a DDOS attack you can set the maximum
    -accepted content length and request field sizes.  The :class:`Request`
    -class has two attributes for that: :attr:`~Request.max_content_length`
    -and :attr:`~Request.max_form_memory_size`.
    -
    -The first one can be used to limit the total content length.  For example
    -by setting it to ``1024 * 1024 * 16`` the request won't accept more than
    -16MB of transmitted data.
    -
    -Because certain data can't be moved to the hard disk (regular post data)
    -whereas temporary files can, there is a second limit you can set.  The
    -:attr:`~Request.max_form_memory_size` limits the size of `POST`
    -transmitted form data.  By setting it to ``1024 * 1024 * 2`` you can make
    -sure that all in memory-stored fields are not more than 2MB in size.
    -
    -This however does *not* affect in-memory stored files if the
    -`stream_factory` used returns a in-memory file.
    +The :class:`Request` class provides a few attributes to control how much data is
    +processed from the request body. This can help mitigate DoS attacks that craft the
    +request in such a way that the server uses too many resources to handle it. Each of
    +these limits will raise a :exc:`~werkzeug.exceptions.RequestEntityTooLarge` if they are
    +exceeded.
    +
    +-   :attr:`~Request.max_content_length` Stop reading request data after this number
    +    of bytes. It's better to configure this in the WSGI server or HTTP server, rather
    +    than the WSGI application.
    +-   :attr:`~Request.max_form_memory_size` Stop reading request data if any form part is
    +    larger than this number of bytes. While file parts can be moved to disk, regular
    +    form field data is stored in memory only.
    +-   :attr:`~Request.max_form_parts` Stop reading request data if more than this number
    +    of parts are sent in multipart form data. This is useful to stop a very large number
    +    of very small parts, especially file parts. The default is 1000.
    +
    +Using Werkzeug to set these limits is only one layer of protection. WSGI servers
    +and HTTPS servers should set their own limits on size and timeouts. The operating system
    +or container manager should set limits on memory and processing time for server
    +processes.
     
     
     How to extend Parsing?
    
  • src/werkzeug/formparser.py+11 1 modified
    @@ -179,6 +179,8 @@ class FormDataParser:
         :param cls: an optional dict class to use.  If this is not specified
                            or `None` the default :class:`MultiDict` is used.
         :param silent: If set to False parsing errors will not be caught.
    +    :param max_form_parts: The maximum number of parts to be parsed. If this is
    +        exceeded, a :exc:`~exceptions.RequestEntityTooLarge` exception is raised.
         """
     
         def __init__(
    @@ -190,6 +192,8 @@ def __init__(
             max_content_length: t.Optional[int] = None,
             cls: t.Optional[t.Type[MultiDict]] = None,
             silent: bool = True,
    +        *,
    +        max_form_parts: t.Optional[int] = None,
         ) -> None:
             if stream_factory is None:
                 stream_factory = default_stream_factory
    @@ -199,6 +203,7 @@ def __init__(
             self.errors = errors
             self.max_form_memory_size = max_form_memory_size
             self.max_content_length = max_content_length
    +        self.max_form_parts = max_form_parts
     
             if cls is None:
                 cls = MultiDict
    @@ -281,6 +286,7 @@ def _parse_multipart(
                 self.errors,
                 max_form_memory_size=self.max_form_memory_size,
                 cls=self.cls,
    +            max_form_parts=self.max_form_parts,
             )
             boundary = options.get("boundary", "").encode("ascii")
     
    @@ -346,10 +352,12 @@ def __init__(
             max_form_memory_size: t.Optional[int] = None,
             cls: t.Optional[t.Type[MultiDict]] = None,
             buffer_size: int = 64 * 1024,
    +        max_form_parts: t.Optional[int] = None,
         ) -> None:
             self.charset = charset
             self.errors = errors
             self.max_form_memory_size = max_form_memory_size
    +        self.max_form_parts = max_form_parts
     
             if stream_factory is None:
                 stream_factory = default_stream_factory
    @@ -409,7 +417,9 @@ def parse(
                 [None],
             )
     
    -        parser = MultipartDecoder(boundary, self.max_form_memory_size)
    +        parser = MultipartDecoder(
    +            boundary, self.max_form_memory_size, max_parts=self.max_form_parts
    +        )
     
             fields = []
             files = []
    
  • src/werkzeug/sansio/multipart.py+8 0 modified
    @@ -87,10 +87,13 @@ def __init__(
             self,
             boundary: bytes,
             max_form_memory_size: Optional[int] = None,
    +        *,
    +        max_parts: Optional[int] = None,
         ) -> None:
             self.buffer = bytearray()
             self.complete = False
             self.max_form_memory_size = max_form_memory_size
    +        self.max_parts = max_parts
             self.state = State.PREAMBLE
             self.boundary = boundary
     
    @@ -118,6 +121,7 @@ def __init__(
                 re.MULTILINE,
             )
             self._search_position = 0
    +        self._parts_decoded = 0
     
         def last_newline(self) -> int:
             try:
    @@ -191,6 +195,10 @@ def next_event(self) -> Event:
                         )
                     self.state = State.DATA
                     self._search_position = 0
    +                self._parts_decoded += 1
    +
    +                if self.max_parts is not None and self._parts_decoded > self.max_parts:
    +                    raise RequestEntityTooLarge()
                 else:
                     # Update the search start position to be equal to the
                     # current buffer length (already searched) minus a
    
  • src/werkzeug/wrappers/request.py+8 0 modified
    @@ -83,6 +83,13 @@ class Request(_SansIORequest):
         #: .. versionadded:: 0.5
         max_form_memory_size: t.Optional[int] = None
     
    +    #: The maximum number of multipart parts to parse, passed to
    +    #: :attr:`form_data_parser_class`. Parsing form data with more than this
    +    #: many parts will raise :exc:`~.RequestEntityTooLarge`.
    +    #:
    +    #: .. versionadded:: 2.2.3
    +    max_form_parts = 1000
    +
         #: The form data parser that should be used.  Can be replaced to customize
         #: the form date parsing.
         form_data_parser_class: t.Type[FormDataParser] = FormDataParser
    @@ -246,6 +253,7 @@ def make_form_data_parser(self) -> FormDataParser:
                 self.max_form_memory_size,
                 self.max_content_length,
                 self.parameter_storage_class,
    +            max_form_parts=self.max_form_parts,
             )
     
         def _load_form_data(self) -> None:
    
  • tests/test_formparser.py+9 0 modified
    @@ -127,6 +127,15 @@ def test_limiting(self):
             req.max_form_memory_size = 400
             assert req.form["foo"] == "Hello World"
     
    +        req = Request.from_values(
    +            input_stream=io.BytesIO(data),
    +            content_length=len(data),
    +            content_type="multipart/form-data; boundary=foo",
    +            method="POST",
    +        )
    +        req.max_form_parts = 1
    +        pytest.raises(RequestEntityTooLarge, lambda: req.form["foo"])
    +
         def test_missing_multipart_boundary(self):
             data = (
                 b"--foo\r\nContent-Disposition: form-field; name=foo\r\n\r\n"
    

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

9

News mentions

0

No linked articles in our index yet.