diff --git a/CVE-2023-23934.patch b/CVE-2023-23934.patch new file mode 100644 index 0000000000000000000000000000000000000000..3f12d8ec782736de90821de90e886742bdcdfb1f --- /dev/null +++ b/CVE-2023-23934.patch @@ -0,0 +1,91 @@ +From 49d56281df8de48a3c48a34e8d914bef31a4125d Mon Sep 17 00:00:00 2001 +From: starlet-dx <15929766099@163.com> +Date: Mon, 14 Aug 2023 17:24:57 +0800 +Subject: [PATCH 1/1] don't strip leading = when parsing cookie + +Reference: +https://github.com/pallets/werkzeug/commit/cf275f42acad1b5950c50ffe8ef58fe62cdce028 +--- + src/werkzeug/_internal.py | 13 +++++++++---- + src/werkzeug/http.py | 4 ---- + tests/test_http.py | 4 +++- + 3 files changed, 12 insertions(+), 9 deletions(-) + +diff --git a/src/werkzeug/_internal.py b/src/werkzeug/_internal.py +index 0c8d0d0..0f63492 100644 +--- a/src/werkzeug/_internal.py ++++ b/src/werkzeug/_internal.py +@@ -35,7 +35,7 @@ _quote_re = re.compile(rb"[\\].") + _legal_cookie_chars_re = rb"[\w\d!#%&\'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]" + _cookie_re = re.compile( + rb""" +- (?P[^=;]+) ++ (?P[^=;]*) + (?:\s*=\s* + (?P + "(?:[^\\"]|\\.)*" | +@@ -460,16 +460,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 45799bf..b7a63d1 100644 +--- a/src/werkzeug/http.py ++++ b/src/werkzeug/http.py +@@ -1227,10 +1227,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 0d6c183..9ffafdf 100644 +--- a/tests/test_http.py ++++ b/tests/test_http.py +@@ -415,7 +415,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*", +@@ -426,6 +427,7 @@ class TestHTTPUtility: + "fo234{": "bar", + "blub": "Blah", + '"__Secure-c"': "d", ++ "__Host-eq": "good", + } + + def test_dump_cookie(self): +-- +2.30.0 + diff --git a/CVE-2023-25577.patch b/CVE-2023-25577.patch new file mode 100644 index 0000000000000000000000000000000000000000..de510b8df951b18c4a83b9b002a4f18352ab052b --- /dev/null +++ b/CVE-2023-25577.patch @@ -0,0 +1,162 @@ +From 1cb968018641a0203c707cefd730da1272df08d5 Mon Sep 17 00:00:00 2001 +From: starlet-dx <15929766099@163.com> +Date: Mon, 14 Aug 2023 17:33:08 +0800 +Subject: [PATCH 1/1] limit the maximum number of multipart form parts + +Reference: +https://github.com/pallets/werkzeug/commit/517cac5a804e8c4dc4ed038bb20dacd038e7a9f1 +--- + src/werkzeug/formparser.py | 12 +++++++++++- + src/werkzeug/sansio/multipart.py | 7 +++++++ + src/werkzeug/wrappers/request.py | 8 ++++++++ + tests/test_formparser.py | 9 +++++++++ + 4 files changed, 35 insertions(+), 1 deletion(-) + +diff --git a/src/werkzeug/formparser.py b/src/werkzeug/formparser.py +index 6cb758f..92f5b3d 100644 +--- a/src/werkzeug/formparser.py ++++ b/src/werkzeug/formparser.py +@@ -181,6 +181,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__( +@@ -192,6 +194,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 +@@ -201,6 +205,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 +@@ -283,6 +288,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") + +@@ -386,10 +392,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 +@@ -449,7 +457,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..31a24d0 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,7 +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: + # No complete boundary in the buffer, but there may be +diff --git a/src/werkzeug/wrappers/request.py b/src/werkzeug/wrappers/request.py +index f68dd5a..113cc41 100644 +--- a/src/werkzeug/wrappers/request.py ++++ b/src/werkzeug/wrappers/request.py +@@ -81,6 +81,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 shoud be used. Can be replaced to customize + #: the form date parsing. + form_data_parser_class: t.Type[FormDataParser] = FormDataParser +@@ -265,6 +272,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 18ed1c0..e32657d 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.30.0 + diff --git a/python-werkzeug.spec b/python-werkzeug.spec index 33dd4166cdc5a8c58f8ce90a936e155955d3ef5b..518581b55269a6c1eb2a688aa0ecc996bdbe688f 100644 --- a/python-werkzeug.spec +++ b/python-werkzeug.spec @@ -1,7 +1,7 @@ %global _empty_manifest_terminate_build 0 Name: python-werkzeug Version: 2.0.3 -Release: 3 +Release: 4 Summary: The comprehensive WSGI web application library. License: BSD-3-Clause URL: https://palletsprojects.com/p/werkzeug/ @@ -9,6 +9,8 @@ Source0: https://files.pythonhosted.org/packages/6c/a8/60514fade2318e277453c9588 Patch0001: backport-fix-typo-and-grammar-mistake.patch Patch0002: backport-fix-flake8-bugbear-finding.patch +Patch0003: CVE-2023-23934.patch +Patch0004: CVE-2023-25577.patch BuildArch: noarch BuildRequires: python3-werkzeug @@ -178,6 +180,9 @@ export PYTHONPATH=$PYTHONPATH:$depath %{_docdir}/* %changelog +* Mon Aug 14 2023 yaoxin - 2.0.3-4 +- Fix CVE-2023-23934 and CVE-2023-25577 + * Mon Jan 9 2023 Bolehu - 2.0.3-3 - fix flake8-bugbear finding