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