python-werkzeug/CVE-2023-25577.patch
starlet-dx 4f09621d4c Fix CVE-2023-23934 and CVE-2023-25577
(cherry picked from commit 3c46d7c0027f7f0ee2582bc196a2611b9b25404d)
2023-08-15 09:51:08 +08:00

163 lines
6.0 KiB
Diff

From 1cb968018641a0203c707cefd730da1272df08d5 Mon Sep 17 00:00:00 2001
From: starlet-dx <15929766099@163.com>
Date: Mon, 14 Aug 2023 17:33:08 +0800
Subject: [PATCH 1/1] limit the maximum number of multipart form parts
Reference:
https://github.com/pallets/werkzeug/commit/517cac5a804e8c4dc4ed038bb20dacd038e7a9f1
---
src/werkzeug/formparser.py | 12 +++++++++++-
src/werkzeug/sansio/multipart.py | 7 +++++++
src/werkzeug/wrappers/request.py | 8 ++++++++
tests/test_formparser.py | 9 +++++++++
4 files changed, 35 insertions(+), 1 deletion(-)
diff --git a/src/werkzeug/formparser.py b/src/werkzeug/formparser.py
index 6cb758f..92f5b3d 100644
--- a/src/werkzeug/formparser.py
+++ b/src/werkzeug/formparser.py
@@ -181,6 +181,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__(
@@ -192,6 +194,8 @@ class FormDataParser:
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
@@ -201,6 +205,7 @@ class FormDataParser:
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
@@ -283,6 +288,7 @@ class FormDataParser:
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")
@@ -386,10 +392,12 @@ class MultiPartParser:
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
@@ -449,7 +457,9 @@ class MultiPartParser:
[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 = []
diff --git a/src/werkzeug/sansio/multipart.py b/src/werkzeug/sansio/multipart.py
index 2d54422..31a24d0 100644
--- a/src/werkzeug/sansio/multipart.py
+++ b/src/werkzeug/sansio/multipart.py
@@ -83,10 +83,13 @@ class MultipartDecoder:
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
@@ -113,6 +116,7 @@ class MultipartDecoder:
% (LINE_BREAK, re.escape(boundary), LINE_BREAK, LINE_BREAK),
re.MULTILINE,
)
+ self._parts_decoded = 0
def last_newline(self) -> int:
try:
@@ -177,7 +181,10 @@ class MultipartDecoder:
name=name,
)
self.state = State.DATA
+ self._parts_decoded += 1
+ if self.max_parts is not None and self._parts_decoded > self.max_parts:
+ raise RequestEntityTooLarge()
elif self.state == State.DATA:
if self.buffer.find(b"--" + self.boundary) == -1:
# No complete boundary in the buffer, but there may be
diff --git a/src/werkzeug/wrappers/request.py b/src/werkzeug/wrappers/request.py
index f68dd5a..113cc41 100644
--- a/src/werkzeug/wrappers/request.py
+++ b/src/werkzeug/wrappers/request.py
@@ -81,6 +81,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 shoud be used. Can be replaced to customize
#: the form date parsing.
form_data_parser_class: t.Type[FormDataParser] = FormDataParser
@@ -265,6 +272,7 @@ class Request(_SansIORequest):
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:
diff --git a/tests/test_formparser.py b/tests/test_formparser.py
index 18ed1c0..e32657d 100644
--- a/tests/test_formparser.py
+++ b/tests/test_formparser.py
@@ -127,6 +127,15 @@ class TestFormParser:
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"
--
2.30.0