|
0 |
From: David Lord <davidism@gmail.com>
|
|
1 |
Date: Tue, 14 Feb 2023 09:08:57 -0800
|
|
2 |
Subject: limit the maximum number of multipart form parts
|
|
3 |
|
|
4 |
Fixes CVE-2023-25577
|
|
5 |
|
|
6 |
Origin: https://github.com/pallets/werkzeug/commit/517cac5a804e8c4dc4ed038bb20dacd038e7a9f1
|
|
7 |
Applied-Upstream: 2.2.3
|
|
8 |
---
|
|
9 |
docs/request_data.rst | 37 ++++++++++++++++++++-----------------
|
|
10 |
src/werkzeug/formparser.py | 12 +++++++++++-
|
|
11 |
src/werkzeug/sansio/multipart.py | 8 ++++++++
|
|
12 |
src/werkzeug/wrappers/request.py | 8 ++++++++
|
|
13 |
tests/test_formparser.py | 9 +++++++++
|
|
14 |
5 files changed, 56 insertions(+), 18 deletions(-)
|
|
15 |
|
|
16 |
diff --git a/docs/request_data.rst b/docs/request_data.rst
|
|
17 |
index 83c6278..e55841e 100644
|
|
18 |
--- a/docs/request_data.rst
|
|
19 |
+++ b/docs/request_data.rst
|
|
20 |
@@ -73,23 +73,26 @@ read the stream *or* call :meth:`~Request.get_data`.
|
|
21 |
Limiting Request Data
|
|
22 |
---------------------
|
|
23 |
|
|
24 |
-To avoid being the victim of a DDOS attack you can set the maximum
|
|
25 |
-accepted content length and request field sizes. The :class:`Request`
|
|
26 |
-class has two attributes for that: :attr:`~Request.max_content_length`
|
|
27 |
-and :attr:`~Request.max_form_memory_size`.
|
|
28 |
-
|
|
29 |
-The first one can be used to limit the total content length. For example
|
|
30 |
-by setting it to ``1024 * 1024 * 16`` the request won't accept more than
|
|
31 |
-16MB of transmitted data.
|
|
32 |
-
|
|
33 |
-Because certain data can't be moved to the hard disk (regular post data)
|
|
34 |
-whereas temporary files can, there is a second limit you can set. The
|
|
35 |
-:attr:`~Request.max_form_memory_size` limits the size of `POST`
|
|
36 |
-transmitted form data. By setting it to ``1024 * 1024 * 2`` you can make
|
|
37 |
-sure that all in memory-stored fields are not more than 2MB in size.
|
|
38 |
-
|
|
39 |
-This however does *not* affect in-memory stored files if the
|
|
40 |
-`stream_factory` used returns a in-memory file.
|
|
41 |
+The :class:`Request` class provides a few attributes to control how much data is
|
|
42 |
+processed from the request body. This can help mitigate DoS attacks that craft the
|
|
43 |
+request in such a way that the server uses too many resources to handle it. Each of
|
|
44 |
+these limits will raise a :exc:`~werkzeug.exceptions.RequestEntityTooLarge` if they are
|
|
45 |
+exceeded.
|
|
46 |
+
|
|
47 |
+- :attr:`~Request.max_content_length` Stop reading request data after this number
|
|
48 |
+ of bytes. It's better to configure this in the WSGI server or HTTP server, rather
|
|
49 |
+ than the WSGI application.
|
|
50 |
+- :attr:`~Request.max_form_memory_size` Stop reading request data if any form part is
|
|
51 |
+ larger than this number of bytes. While file parts can be moved to disk, regular
|
|
52 |
+ form field data is stored in memory only.
|
|
53 |
+- :attr:`~Request.max_form_parts` Stop reading request data if more than this number
|
|
54 |
+ of parts are sent in multipart form data. This is useful to stop a very large number
|
|
55 |
+ of very small parts, especially file parts. The default is 1000.
|
|
56 |
+
|
|
57 |
+Using Werkzeug to set these limits is only one layer of protection. WSGI servers
|
|
58 |
+and HTTPS servers should set their own limits on size and timeouts. The operating system
|
|
59 |
+or container manager should set limits on memory and processing time for server
|
|
60 |
+processes.
|
|
61 |
|
|
62 |
|
|
63 |
How to extend Parsing?
|
|
64 |
diff --git a/src/werkzeug/formparser.py b/src/werkzeug/formparser.py
|
|
65 |
index 10d58ca..bebb2fc 100644
|
|
66 |
--- a/src/werkzeug/formparser.py
|
|
67 |
+++ b/src/werkzeug/formparser.py
|
|
68 |
@@ -179,6 +179,8 @@ class FormDataParser:
|
|
69 |
:param cls: an optional dict class to use. If this is not specified
|
|
70 |
or `None` the default :class:`MultiDict` is used.
|
|
71 |
:param silent: If set to False parsing errors will not be caught.
|
|
72 |
+ :param max_form_parts: The maximum number of parts to be parsed. If this is
|
|
73 |
+ exceeded, a :exc:`~exceptions.RequestEntityTooLarge` exception is raised.
|
|
74 |
"""
|
|
75 |
|
|
76 |
def __init__(
|
|
77 |
@@ -190,6 +192,8 @@ class FormDataParser:
|
|
78 |
max_content_length: t.Optional[int] = None,
|
|
79 |
cls: t.Optional[t.Type[MultiDict]] = None,
|
|
80 |
silent: bool = True,
|
|
81 |
+ *,
|
|
82 |
+ max_form_parts: t.Optional[int] = None,
|
|
83 |
) -> None:
|
|
84 |
if stream_factory is None:
|
|
85 |
stream_factory = default_stream_factory
|
|
86 |
@@ -199,6 +203,7 @@ class FormDataParser:
|
|
87 |
self.errors = errors
|
|
88 |
self.max_form_memory_size = max_form_memory_size
|
|
89 |
self.max_content_length = max_content_length
|
|
90 |
+ self.max_form_parts = max_form_parts
|
|
91 |
|
|
92 |
if cls is None:
|
|
93 |
cls = MultiDict
|
|
94 |
@@ -281,6 +286,7 @@ class FormDataParser:
|
|
95 |
self.errors,
|
|
96 |
max_form_memory_size=self.max_form_memory_size,
|
|
97 |
cls=self.cls,
|
|
98 |
+ max_form_parts=self.max_form_parts,
|
|
99 |
)
|
|
100 |
boundary = options.get("boundary", "").encode("ascii")
|
|
101 |
|
|
102 |
@@ -346,10 +352,12 @@ class MultiPartParser:
|
|
103 |
max_form_memory_size: t.Optional[int] = None,
|
|
104 |
cls: t.Optional[t.Type[MultiDict]] = None,
|
|
105 |
buffer_size: int = 64 * 1024,
|
|
106 |
+ max_form_parts: t.Optional[int] = None,
|
|
107 |
) -> None:
|
|
108 |
self.charset = charset
|
|
109 |
self.errors = errors
|
|
110 |
self.max_form_memory_size = max_form_memory_size
|
|
111 |
+ self.max_form_parts = max_form_parts
|
|
112 |
|
|
113 |
if stream_factory is None:
|
|
114 |
stream_factory = default_stream_factory
|
|
115 |
@@ -409,7 +417,9 @@ class MultiPartParser:
|
|
116 |
[None],
|
|
117 |
)
|
|
118 |
|
|
119 |
- parser = MultipartDecoder(boundary, self.max_form_memory_size)
|
|
120 |
+ parser = MultipartDecoder(
|
|
121 |
+ boundary, self.max_form_memory_size, max_parts=self.max_form_parts
|
|
122 |
+ )
|
|
123 |
|
|
124 |
fields = []
|
|
125 |
files = []
|
|
126 |
diff --git a/src/werkzeug/sansio/multipart.py b/src/werkzeug/sansio/multipart.py
|
|
127 |
index d8abeb3..2684e5d 100644
|
|
128 |
--- a/src/werkzeug/sansio/multipart.py
|
|
129 |
+++ b/src/werkzeug/sansio/multipart.py
|
|
130 |
@@ -87,10 +87,13 @@ class MultipartDecoder:
|
|
131 |
self,
|
|
132 |
boundary: bytes,
|
|
133 |
max_form_memory_size: Optional[int] = None,
|
|
134 |
+ *,
|
|
135 |
+ max_parts: Optional[int] = None,
|
|
136 |
) -> None:
|
|
137 |
self.buffer = bytearray()
|
|
138 |
self.complete = False
|
|
139 |
self.max_form_memory_size = max_form_memory_size
|
|
140 |
+ self.max_parts = max_parts
|
|
141 |
self.state = State.PREAMBLE
|
|
142 |
self.boundary = boundary
|
|
143 |
|
|
144 |
@@ -118,6 +121,7 @@ class MultipartDecoder:
|
|
145 |
re.MULTILINE,
|
|
146 |
)
|
|
147 |
self._search_position = 0
|
|
148 |
+ self._parts_decoded = 0
|
|
149 |
|
|
150 |
def last_newline(self) -> int:
|
|
151 |
try:
|
|
152 |
@@ -191,6 +195,10 @@ class MultipartDecoder:
|
|
153 |
)
|
|
154 |
self.state = State.DATA
|
|
155 |
self._search_position = 0
|
|
156 |
+ self._parts_decoded += 1
|
|
157 |
+
|
|
158 |
+ if self.max_parts is not None and self._parts_decoded > self.max_parts:
|
|
159 |
+ raise RequestEntityTooLarge()
|
|
160 |
else:
|
|
161 |
# Update the search start position to be equal to the
|
|
162 |
# current buffer length (already searched) minus a
|
|
163 |
diff --git a/src/werkzeug/wrappers/request.py b/src/werkzeug/wrappers/request.py
|
|
164 |
index 57b739c..a6d5429 100644
|
|
165 |
--- a/src/werkzeug/wrappers/request.py
|
|
166 |
+++ b/src/werkzeug/wrappers/request.py
|
|
167 |
@@ -83,6 +83,13 @@ class Request(_SansIORequest):
|
|
168 |
#: .. versionadded:: 0.5
|
|
169 |
max_form_memory_size: t.Optional[int] = None
|
|
170 |
|
|
171 |
+ #: The maximum number of multipart parts to parse, passed to
|
|
172 |
+ #: :attr:`form_data_parser_class`. Parsing form data with more than this
|
|
173 |
+ #: many parts will raise :exc:`~.RequestEntityTooLarge`.
|
|
174 |
+ #:
|
|
175 |
+ #: .. versionadded:: 2.2.3
|
|
176 |
+ max_form_parts = 1000
|
|
177 |
+
|
|
178 |
#: The form data parser that should be used. Can be replaced to customize
|
|
179 |
#: the form date parsing.
|
|
180 |
form_data_parser_class: t.Type[FormDataParser] = FormDataParser
|
|
181 |
@@ -246,6 +253,7 @@ class Request(_SansIORequest):
|
|
182 |
self.max_form_memory_size,
|
|
183 |
self.max_content_length,
|
|
184 |
self.parameter_storage_class,
|
|
185 |
+ max_form_parts=self.max_form_parts,
|
|
186 |
)
|
|
187 |
|
|
188 |
def _load_form_data(self) -> None:
|
|
189 |
diff --git a/tests/test_formparser.py b/tests/test_formparser.py
|
|
190 |
index 49010b4..4c518b1 100644
|
|
191 |
--- a/tests/test_formparser.py
|
|
192 |
+++ b/tests/test_formparser.py
|
|
193 |
@@ -127,6 +127,15 @@ class TestFormParser:
|
|
194 |
req.max_form_memory_size = 400
|
|
195 |
assert req.form["foo"] == "Hello World"
|
|
196 |
|
|
197 |
+ req = Request.from_values(
|
|
198 |
+ input_stream=io.BytesIO(data),
|
|
199 |
+ content_length=len(data),
|
|
200 |
+ content_type="multipart/form-data; boundary=foo",
|
|
201 |
+ method="POST",
|
|
202 |
+ )
|
|
203 |
+ req.max_form_parts = 1
|
|
204 |
+ pytest.raises(RequestEntityTooLarge, lambda: req.form["foo"])
|
|
205 |
+
|
|
206 |
def test_missing_multipart_boundary(self):
|
|
207 |
data = (
|
|
208 |
b"--foo\r\nContent-Disposition: form-field; name=foo\r\n\r\n"
|