aboutsummaryrefslogtreecommitdiffstats
path: root/meta-python/recipes-devtools/python/python3-werkzeug/CVE-2023-25577.patch
blob: 61551d8fcab56ea0e41ff599c2bb3ea2dc536c9f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
From 5a56cdcbaec2153cd67596c6c2c8056e1ea5ed56 Mon Sep 17 00:00:00 2001
From: David Lord <davidism@gmail.com>
Date: Tue, 2 May 2023 11:31:10 +0000
Subject: [PATCH] Merge pull request from GHSA-xg9f-g7g7-2323

limit the maximum number of multipart form parts

CVE: CVE-2023-25577

Upstream-Status: Backport [https://github.com/pallets/werkzeug/commit/517cac5a804e8c4dc4ed038bb20dacd038e7a9f1]

Signed-off-by: Narpat Mali <narpat.mali@windriver.com>
---
 CHANGES.rst                      |  5 +++++
 docs/request_data.rst            | 37 +++++++++++++++++---------------
 src/werkzeug/formparser.py       | 12 ++++++++++-
 src/werkzeug/sansio/multipart.py |  8 +++++++
 src/werkzeug/wrappers/request.py |  8 +++++++
 tests/test_formparser.py         |  9 ++++++++
 6 files changed, 61 insertions(+), 18 deletions(-)

diff --git a/CHANGES.rst b/CHANGES.rst
index a351d7c..6e809ba 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,10 @@
 .. currentmodule:: werkzeug

+-   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.1.1
 -------------

diff --git a/docs/request_data.rst b/docs/request_data.rst
index 83c6278..e55841e 100644
--- a/docs/request_data.rst
+++ b/docs/request_data.rst
@@ -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?
diff --git a/src/werkzeug/formparser.py b/src/werkzeug/formparser.py
index 10d58ca..bebb2fc 100644
--- a/src/werkzeug/formparser.py
+++ b/src/werkzeug/formparser.py
@@ -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 @@ 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
@@ -199,6 +203,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
@@ -281,6 +286,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")

@@ -346,10 +352,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
@@ -409,7 +417,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..e7d742b 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,6 +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:
diff --git a/src/werkzeug/wrappers/request.py b/src/werkzeug/wrappers/request.py
index 57b739c..a6d5429 100644
--- a/src/werkzeug/wrappers/request.py
+++ b/src/werkzeug/wrappers/request.py
@@ -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 @@ 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 5fc803e..834324f 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.40.0