python-werkzeug/CVE-2023-25577.patch
starlet-dx 6e2b684b61 Fix CVE-2023-23934 and CVE-2023-25577
(cherry picked from commit a28c3782e3a95a6bb9e97f75cd0eb29523573403)
2023-08-15 11:02:06 +08:00

147 lines
5.6 KiB
Diff

From dbd3af18b9e7e6e081181594fd6a9fb8daa01a91 Mon Sep 17 00:00:00 2001
From: starlet-dx <15929766099@163.com>
Date: Tue, 15 Aug 2023 10:04:11 +0800
Subject: [PATCH 1/1] limit the maximum number of multipart form parts
Origin:
https://github.com/pallets/werkzeug/commit/fe899d0cdf767a7289a8bf746b7f72c2907a1b4b
---
src/werkzeug/formparser.py | 16 ++++++++++++++--
src/werkzeug/wrappers/base_request.py | 8 ++++++++
tests/test_formparser.py | 9 +++++++++
3 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/src/werkzeug/formparser.py b/src/werkzeug/formparser.py
index ffdb9b0..40959fb 100644
--- a/src/werkzeug/formparser.py
+++ b/src/werkzeug/formparser.py
@@ -168,6 +168,8 @@ 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__(
@@ -179,6 +181,7 @@ class FormDataParser(object):
max_content_length=None,
cls=None,
silent=True,
+ max_form_parts=None,
):
if stream_factory is None:
stream_factory = default_stream_factory
@@ -191,6 +194,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)
@@ -244,6 +248,7 @@ class FormDataParser(object):
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")
if boundary is None:
@@ -333,10 +338,12 @@ class MultiPartParser(object):
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
)
@@ -528,11 +535,12 @@ class MultiPartParser(object):
yield _end, None
- def parse_parts(self, file, boundary, content_length):
+ def parse_parts(self, file, boundary, content_length, max_parts=None):
"""Generate ``('file', (name, val))`` and
``('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:
@@ -562,6 +570,9 @@ class MultiPartParser(object):
self.in_memory_threshold_reached(in_memory)
elif ellt == _end:
+ _parts_decoded += 1
+ if max_parts is not None and _parts_decoded > max_parts:
+ raise exceptions.RequestEntityTooLarge()
if is_file:
container.seek(0)
yield (
@@ -577,7 +588,8 @@ class MultiPartParser(object):
def parse(self, file, boundary, content_length):
formstream, filestream = tee(
- self.parse_parts(file, boundary, content_length), 2
+ self.parse_parts(file, boundary, content_length,
+ max_parts=self.max_form_parts), 2
)
form = (p[1] for p in formstream if p[0] == "form")
files = (p[1] for p in filestream if p[0] == "file")
diff --git a/src/werkzeug/wrappers/base_request.py b/src/werkzeug/wrappers/base_request.py
index 1f21db2..0c1e221 100644
--- a/src/werkzeug/wrappers/base_request.py
+++ b/src/werkzeug/wrappers/base_request.py
@@ -98,6 +98,13 @@ 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`.
+ #:
+ #: .. versionadded:: 2.2.3
+ 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
@@ -293,6 +300,7 @@ class BaseRequest(object):
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):
diff --git a/tests/test_formparser.py b/tests/test_formparser.py
index 6d85838..2a8d635 100644
--- a/tests/test_formparser.py
+++ b/tests/test_formparser.py
@@ -124,6 +124,15 @@ class TestFormParser(object):
req.max_form_memory_size = 400
strict_eq(req.form["foo"], u"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"
--
2.30.0