Codebase list python-werkzeug / d2b5b41
Fix security vulnerabilities Robin Gustafsson authored 1 year, 12 days ago Thomas Goirand committed 1 year, 12 days ago
4 changed file(s) with 307 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 python-werkzeug (2.2.2-3) UNRELEASED; urgency=medium
1
2 * Fix security vulnerabilities
3 (CVE-2023-23934, CVE-2023-25577, Closes: #1031370)
4
5 -- Robin Gustafsson <robin@rgson.se> Thu, 20 Apr 2023 21:17:00 +0200
6
07 python-werkzeug (2.2.2-2) unstable; urgency=medium
18
29 * Uploading to unstable.
0 From: David Lord <davidism@gmail.com>
1 Date: Tue, 31 Jan 2023 14:29:34 -0800
2 Subject: don't strip leading = when parsing cookie
3
4 Fixes CVE-2023-23934
5
6 Origin: https://github.com/pallets/werkzeug/commit/cf275f42acad1b5950c50ffe8ef58fe62cdce028
7 Applied-Upstream: 2.2.3
8 ---
9 src/werkzeug/_internal.py | 13 +++++++++----
10 src/werkzeug/sansio/http.py | 4 ----
11 tests/test_http.py | 4 +++-
12 3 files changed, 12 insertions(+), 9 deletions(-)
13
14 diff --git a/src/werkzeug/_internal.py b/src/werkzeug/_internal.py
15 index 4636647..f95207a 100644
16 --- a/src/werkzeug/_internal.py
17 +++ b/src/werkzeug/_internal.py
18 @@ -34,7 +34,7 @@ _quote_re = re.compile(rb"[\\].")
19 _legal_cookie_chars_re = rb"[\w\d!#%&\'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]"
20 _cookie_re = re.compile(
21 rb"""
22 - (?P<key>[^=;]+)
23 + (?P<key>[^=;]*)
24 (?:\s*=\s*
25 (?P<val>
26 "(?:[^\\"]|\\.)*" |
27 @@ -382,16 +382,21 @@ def _cookie_parse_impl(b: bytes) -> t.Iterator[t.Tuple[bytes, bytes]]:
28 """Lowlevel cookie parsing facility that operates on bytes."""
29 i = 0
30 n = len(b)
31 + b += b";"
32
33 while i < n:
34 - match = _cookie_re.search(b + b";", i)
35 + match = _cookie_re.match(b, i)
36 +
37 if not match:
38 break
39
40 - key = match.group("key").strip()
41 - value = match.group("val") or b""
42 i = match.end(0)
43 + key = match.group("key").strip()
44 +
45 + if not key:
46 + continue
47
48 + value = match.group("val") or b""
49 yield key, _cookie_unquote(value)
50
51
52 diff --git a/src/werkzeug/sansio/http.py b/src/werkzeug/sansio/http.py
53 index 8288882..6b22738 100644
54 --- a/src/werkzeug/sansio/http.py
55 +++ b/src/werkzeug/sansio/http.py
56 @@ -126,10 +126,6 @@ def parse_cookie(
57 def _parse_pairs() -> t.Iterator[t.Tuple[str, str]]:
58 for key, val in _cookie_parse_impl(cookie): # type: ignore
59 key_str = _to_str(key, charset, errors, allow_none_charset=True)
60 -
61 - if not key_str:
62 - continue
63 -
64 val_str = _to_str(val, charset, errors, allow_none_charset=True)
65 yield key_str, val_str
66
67 diff --git a/tests/test_http.py b/tests/test_http.py
68 index 3760dc1..999549e 100644
69 --- a/tests/test_http.py
70 +++ b/tests/test_http.py
71 @@ -411,7 +411,8 @@ class TestHTTPUtility:
72 def test_parse_cookie(self):
73 cookies = http.parse_cookie(
74 "dismiss-top=6; CP=null*; PHPSESSID=0a539d42abc001cdc762809248d4beed;"
75 - 'a=42; b="\\";"; ; fo234{=bar;blub=Blah; "__Secure-c"=d'
76 + 'a=42; b="\\";"; ; fo234{=bar;blub=Blah; "__Secure-c"=d;'
77 + "==__Host-eq=bad;__Host-eq=good;"
78 )
79 assert cookies.to_dict() == {
80 "CP": "null*",
81 @@ -422,6 +423,7 @@ class TestHTTPUtility:
82 "fo234{": "bar",
83 "blub": "Blah",
84 '"__Secure-c"': "d",
85 + "__Host-eq": "good",
86 }
87
88 def test_dump_cookie(self):
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"
00 preserve-any-existing-PYTHONPATH-in-tests.patch
11 remove-test_exclude_patterns-test.patch
2 0003-don-t-strip-leading-when-parsing-cookie.patch
3 0004-limit-the-maximum-number-of-multipart-form-parts.patch