diff options
Diffstat (limited to 'meta-python/recipes-devtools/python/python3-werkzeug')
-rw-r--r-- | meta-python/recipes-devtools/python/python3-werkzeug/CVE-2023-23934.patch | 117 | ||||
-rw-r--r-- | meta-python/recipes-devtools/python/python3-werkzeug/CVE-2023-25577.patch | 231 |
2 files changed, 348 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-werkzeug/CVE-2023-23934.patch b/meta-python/recipes-devtools/python/python3-werkzeug/CVE-2023-23934.patch new file mode 100644 index 0000000000..3a0f4324a1 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-werkzeug/CVE-2023-23934.patch @@ -0,0 +1,117 @@ +From db1457abec7fe27148673f5f8bfdf5c52eb7f29f Mon Sep 17 00:00:00 2001 +From: David Lord <davidism@gmail.com> +Date: Wed, 10 May 2023 11:33:18 +0000 +Subject: [PATCH] Merge pull request from GHSA-px8h-6qxv-m22q + +don't strip leading `=` when parsing cookie + +"src/werkzeug/sansio/http.py" file is not available in the current recipe +version 2.1.1 and this has been introduced from 2.2.0 version. Before 2.2.0 +version, this http.py file was only available in the "src/werkzeug/http.py" +and we could see the same functions available there which are getting modified +in the CVE fix commit. Hence, modifying the same at "src/werkzeug/http.py" file. + +CVE: CVE-2023-23934 + +Upstream-Status: Backport [https://github.com/pallets/werkzeug/commit/cf275f42acad1b5950c50ffe8ef58fe62cdce028] + +Signed-off-by: Narpat Mali <narpat.mali@windriver.com> +--- + CHANGES.rst | 3 +++ + src/werkzeug/_internal.py | 13 +++++++++---- + src/werkzeug/http.py | 4 ---- + tests/test_http.py | 4 +++- + 4 files changed, 15 insertions(+), 9 deletions(-) + +diff --git a/CHANGES.rst b/CHANGES.rst +index 6e809ba..13ef75b 100644 +--- a/CHANGES.rst ++++ b/CHANGES.rst +@@ -4,6 +4,9 @@ + ``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. ++- A cookie header that starts with ``=`` is treated as an empty key and discarded, ++ rather than stripping the leading ``==``. ++ + + Version 2.1.1 + ------------- +diff --git a/src/werkzeug/_internal.py b/src/werkzeug/_internal.py +index a8b3523..d6290ba 100644 +--- a/src/werkzeug/_internal.py ++++ b/src/werkzeug/_internal.py +@@ -34,7 +34,7 @@ _quote_re = re.compile(rb"[\\].") + _legal_cookie_chars_re = rb"[\w\d!#%&\'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]" + _cookie_re = re.compile( + rb""" +- (?P<key>[^=;]+) ++ (?P<key>[^=;]*) + (?:\s*=\s* + (?P<val> + "(?:[^\\"]|\\.)*" | +@@ -382,16 +382,21 @@ def _cookie_parse_impl(b: bytes) -> t.Iterator[t.Tuple[bytes, bytes]]: + """Lowlevel cookie parsing facility that operates on bytes.""" + i = 0 + n = len(b) ++ b += b";" + + while i < n: +- match = _cookie_re.search(b + b";", i) ++ match = _cookie_re.match(b, i) ++ + if not match: + break + +- key = match.group("key").strip() +- value = match.group("val") or b"" + i = match.end(0) ++ key = match.group("key").strip() ++ ++ if not key: ++ continue + ++ value = match.group("val") or b"" + yield key, _cookie_unquote(value) + + +diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py +index 9369900..ae133e3 100644 +--- a/src/werkzeug/http.py ++++ b/src/werkzeug/http.py +@@ -1205,10 +1205,6 @@ def parse_cookie( + def _parse_pairs() -> t.Iterator[t.Tuple[str, str]]: + for key, val in _cookie_parse_impl(header): # type: ignore + key_str = _to_str(key, charset, errors, allow_none_charset=True) +- +- if not key_str: +- continue +- + val_str = _to_str(val, charset, errors, allow_none_charset=True) + yield key_str, val_str + +diff --git a/tests/test_http.py b/tests/test_http.py +index 5936bfa..59cc179 100644 +--- a/tests/test_http.py ++++ b/tests/test_http.py +@@ -427,7 +427,8 @@ class TestHTTPUtility: + def test_parse_cookie(self): + cookies = http.parse_cookie( + "dismiss-top=6; CP=null*; PHPSESSID=0a539d42abc001cdc762809248d4beed;" +- 'a=42; b="\\";"; ; fo234{=bar;blub=Blah; "__Secure-c"=d' ++ 'a=42; b="\\";"; ; fo234{=bar;blub=Blah; "__Secure-c"=d;' ++ "==__Host-eq=bad;__Host-eq=good;" + ) + assert cookies.to_dict() == { + "CP": "null*", +@@ -438,6 +439,7 @@ class TestHTTPUtility: + "fo234{": "bar", + "blub": "Blah", + '"__Secure-c"': "d", ++ "__Host-eq": "good", + } + + def test_dump_cookie(self): +-- +2.40.0 + 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 @@ +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 |