From c38f41e4c9ca0bcfc18c273fc955ffb814607332 Mon Sep 17 00:00:00 2001
From: Darragh O'Reilly <doreilly@suse.com>
Date: Mon, 13 Mar 2023 13:47:41 +0000
Subject: [PATCH] limit the maximum number of multipart form parts

Backport of
https://github.com/pallets/werkzeug/commit/fe899d0cdf767a7289a8bf746b7f72c2907a1b4b
---
 tests/test_formparser.py |  8 ++++++++
 werkzeug/formparser.py   | 16 +++++++++++++---
 werkzeug/wrappers.py     |  9 ++++++++-
 3 files changed, 29 insertions(+), 4 deletions(-)

diff --git a/tests/test_formparser.py b/tests/test_formparser.py
index c140476b..4717d8c3 100644
--- a/tests/test_formparser.py
+++ b/tests/test_formparser.py
@@ -101,6 +101,14 @@ class TestFormParser(object):
         req.max_form_memory_size = 400
         strict_eq(req.form['foo'], u'Hello World')
 
+        req = Request.from_values(input_stream=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'
                 b'Hello World\r\n'
diff --git a/werkzeug/formparser.py b/werkzeug/formparser.py
index a0118054..4a757308 100644
--- a/werkzeug/formparser.py
+++ b/werkzeug/formparser.py
@@ -137,12 +137,14 @@ class FormDataParser(object):
     :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__(self, stream_factory=None, charset='utf-8',
                  errors='replace', max_form_memory_size=None,
                  max_content_length=None, cls=None,
-                 silent=True):
+                 silent=True, max_form_parts=None):
         if stream_factory is None:
             stream_factory = default_stream_factory
         self.stream_factory = stream_factory
@@ -154,6 +156,7 @@ class FormDataParser(object):
             cls = MultiDict
         self.cls = cls
         self.silent = silent
+        self.max_form_parts = max_form_parts
 
     def get_parse_func(self, mimetype, options):
         return self.parse_functions.get(mimetype)
@@ -203,7 +206,7 @@ class FormDataParser(object):
     def _parse_multipart(self, stream, mimetype, content_length, options):
         parser = MultiPartParser(self.stream_factory, self.charset, self.errors,
                                  max_form_memory_size=self.max_form_memory_size,
-                                 cls=self.cls)
+                                 cls=self.cls, max_form_parts=self.max_form_parts)
         boundary = options.get('boundary')
         if boundary is None:
             raise ValueError('Missing boundary')
@@ -285,10 +288,12 @@ _end = 'end'
 class MultiPartParser(object):
 
     def __init__(self, stream_factory=None, charset='utf-8', errors='replace',
-                 max_form_memory_size=None, cls=None, buffer_size=64 * 1024):
+                 max_form_memory_size=None, cls=None, buffer_size=64 * 1024,
+                 max_form_parts=None):
         self.charset = charset
         self.errors = errors
         self.max_form_memory_size = max_form_memory_size
+        self.max_form_parts = max_form_parts
         self.stream_factory = default_stream_factory if stream_factory is None else stream_factory
         self.cls = MultiDict if cls is None else cls
 
@@ -472,6 +477,7 @@ class MultiPartParser(object):
         ``('form', (name, val))`` parts.
         """
         in_memory = 0
+        parts_decoded = 0
 
         for ellt, ell in self.parse_lines(file, boundary, content_length):
             if ellt == _begin_file:
@@ -500,6 +506,10 @@ class MultiPartParser(object):
                         self.in_memory_threshold_reached(in_memory)
 
             elif ellt == _end:
+                parts_decoded += 1
+                if self.max_form_parts is not None and \
+                   parts_decoded > self.max_form_parts:
+                    raise exceptions.RequestEntityTooLarge()
                 if is_file:
                     container.seek(0)
                     yield ('file',
diff --git a/werkzeug/wrappers.py b/werkzeug/wrappers.py
index ea35228e..d5a40b84 100644
--- a/werkzeug/wrappers.py
+++ b/werkzeug/wrappers.py
@@ -169,6 +169,12 @@ class BaseRequest(object):
     #: .. versionadded:: 0.5
     max_form_memory_size = 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`.
+    #:
+    max_form_parts = 1000
+
     #: the class to use for `args` and `form`.  The default is an
     #: :class:`~werkzeug.datastructures.ImmutableMultiDict` which supports
     #: multiple values per key.  alternatively it makes sense to use an
@@ -345,7 +351,8 @@ class BaseRequest(object):
                                            self.encoding_errors,
                                            self.max_form_memory_size,
                                            self.max_content_length,
-                                           self.parameter_storage_class)
+                                           self.parameter_storage_class,
+                                           max_form_parts=self.max_form_parts)
 
     def _load_form_data(self):
         """Method used internally to retrieve submitted data.  After calling
-- 
2.34.1

