Version in base suite: 1.0.1+dfsg1-2 Base version: python-werkzeug_1.0.1+dfsg1-2 Target version: python-werkzeug_1.0.1+dfsg1-2+deb11u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/python-werkzeug/python-werkzeug_1.0.1+dfsg1-2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/python-werkzeug/python-werkzeug_1.0.1+dfsg1-2+deb11u1.dsc changelog | 10 patches/don-t-strip-leading-when-parsing-cookie.patch | 68 ++++ patches/limit-the-maximum-number-of-multipart-form-parts.patch | 156 ++++++++++ patches/series | 2 4 files changed, 236 insertions(+) diff -Nru python-werkzeug-1.0.1+dfsg1/debian/changelog python-werkzeug-1.0.1+dfsg1/debian/changelog --- python-werkzeug-1.0.1+dfsg1/debian/changelog 2020-07-14 07:40:21.000000000 +0000 +++ python-werkzeug-1.0.1+dfsg1/debian/changelog 2023-06-14 13:06:21.000000000 +0000 @@ -1,3 +1,13 @@ +python-werkzeug (1.0.1+dfsg1-2+deb11u1) bullseye-security; urgency=high + + * Non-maintainer upload by the Security Team. + * don't strip leading = when parsing cookie (CVE-2023-23934) + (Closes: #1031370) + * limit the maximum number of multipart form parts (CVE-2023-25577) + (Closes: #1031370) + + -- Salvatore Bonaccorso Wed, 14 Jun 2023 15:06:21 +0200 + python-werkzeug (1.0.1+dfsg1-2) unstable; urgency=medium * Uploading to unstable. diff -Nru python-werkzeug-1.0.1+dfsg1/debian/patches/don-t-strip-leading-when-parsing-cookie.patch python-werkzeug-1.0.1+dfsg1/debian/patches/don-t-strip-leading-when-parsing-cookie.patch --- python-werkzeug-1.0.1+dfsg1/debian/patches/don-t-strip-leading-when-parsing-cookie.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-werkzeug-1.0.1+dfsg1/debian/patches/don-t-strip-leading-when-parsing-cookie.patch 2023-06-14 07:20:38.000000000 +0000 @@ -0,0 +1,68 @@ +From: David Lord +Date: Tue, 31 Jan 2023 14:29:34 -0800 +Subject: don't strip leading = when parsing cookie +Origin: https://github.com/pallets/werkzeug/commit/8c2b4b82d0cade0d37e6a88e2cd2413878e8ebd4 +Bug-Debian: https://bugs.debian.org/1031370 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2023-23934 + +--- + CHANGES.rst | 2 ++ + src/werkzeug/_internal.py | 13 +++++++++---- + src/werkzeug/sansio/http.py | 4 ---- + tests/test_http.py | 4 +++- + 4 files changed, 14 insertions(+), 9 deletions(-) + +--- a/src/werkzeug/_internal.py ++++ b/src/werkzeug/_internal.py +@@ -40,7 +40,7 @@ _quote_re = re.compile(br"[\\].") + _legal_cookie_chars_re = br"[\w\d!#%&\'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]" + _cookie_re = re.compile( + br""" +- (?P[^=;]+) ++ (?P[^=;]*) + (?:\s*=\s* + (?P + "(?:[^\\"]|\\.)*" | +@@ -316,16 +316,21 @@ def _cookie_parse_impl(b): + """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 _cookie_unquote(key), _cookie_unquote(value) + + +--- a/tests/test_http.py ++++ b/tests/test_http.py +@@ -446,6 +446,7 @@ class TestHTTPUtility(object): + cookies = http.parse_cookie( + "dismiss-top=6; CP=null*; PHPSESSID=0a539d42abc001cdc762809248d4beed;" + ' a=42; b="\\";"; ; fo234{=bar;blub=Blah;' ++ "==__Host-eq=bad;__Host-eq=good;" + ) + assert cookies.to_dict() == { + "CP": u"null*", +@@ -455,6 +456,7 @@ class TestHTTPUtility(object): + "b": u'";', + "fo234{": u"bar", + "blub": u"Blah", ++ "__Host-eq": "good", + } + + def test_dump_cookie(self): diff -Nru python-werkzeug-1.0.1+dfsg1/debian/patches/limit-the-maximum-number-of-multipart-form-parts.patch python-werkzeug-1.0.1+dfsg1/debian/patches/limit-the-maximum-number-of-multipart-form-parts.patch --- python-werkzeug-1.0.1+dfsg1/debian/patches/limit-the-maximum-number-of-multipart-form-parts.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-werkzeug-1.0.1+dfsg1/debian/patches/limit-the-maximum-number-of-multipart-form-parts.patch 2023-06-14 13:06:21.000000000 +0000 @@ -0,0 +1,156 @@ +From: pgjones +Date: Sun, 29 Jan 2023 15:17:37 +0000 +Subject: limit the maximum number of multipart form parts +Origin: https://github.com/pallets/werkzeug/commit/fe899d0cdf767a7289a8bf746b7f72c2907a1b4b +Bug-Debian: https://bugs.debian.org/1031370 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2023-25577 + +Add a limit to the number of multipart form data parts the parser will +attempt to parse. If the limit is exceeded, it raises +`RequestEntityTooLargeError`. + +A default of 1000 seems large enough to allow legitimate use cases while +preventing the previous unlimited parsing. This differs from similar +settings that are unset by default, as I think safe defaults are better +practice. + +The limit can be adjusted per request by changing it on the request +object before parsing. For example, it can be set based on what you +expect a given endpoint to handle. + +```python +req.max_form_parts = 20 +form = req.form +``` +--- + CHANGES.rst | 4 ++++ + src/werkzeug/formparser.py | 13 ++++++++++++- + src/werkzeug/sansio/multipart.py | 7 +++++++ + src/werkzeug/wrappers/request.py | 11 +++++++++++ + tests/test_formparser.py | 9 +++++++++ + 5 files changed, 43 insertions(+), 1 deletion(-) + +--- a/src/werkzeug/formparser.py ++++ b/src/werkzeug/formparser.py +@@ -168,6 +168,8 @@ class FormDataParser(object): + :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__( +@@ -179,6 +181,7 @@ class FormDataParser(object): + max_content_length=None, + cls=None, + silent=True, ++ max_form_parts=None, + ): + if stream_factory is None: + stream_factory = default_stream_factory +@@ -191,6 +194,7 @@ class FormDataParser(object): + cls = MultiDict + self.cls = cls + self.silent = silent ++ self.max_form_parts = max_form_parts + + def get_parse_func(self, mimetype, options): + return self.parse_functions.get(mimetype) +@@ -244,6 +248,7 @@ class FormDataParser(object): + 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") + if boundary is None: +@@ -333,10 +338,12 @@ class MultiPartParser(object): + max_form_memory_size=None, + cls=None, + buffer_size=64 * 1024, ++ max_form_parts=None, + ): + self.charset = charset + self.errors = errors + self.max_form_memory_size = max_form_memory_size ++ self.max_form_parts = max_form_parts + self.stream_factory = ( + default_stream_factory if stream_factory is None else stream_factory + ) +@@ -528,11 +535,12 @@ class MultiPartParser(object): + + yield _end, None + +- def parse_parts(self, file, boundary, content_length): ++ def parse_parts(self, file, boundary, content_length, max_parts=None): + """Generate ``('file', (name, val))`` and + ``('form', (name, val))`` parts. + """ + in_memory = 0 ++ _parts_decoded = 0 + + for ellt, ell in self.parse_lines(file, boundary, content_length): + if ellt == _begin_file: +@@ -562,6 +570,9 @@ class MultiPartParser(object): + self.in_memory_threshold_reached(in_memory) + + elif ellt == _end: ++ _parts_decoded += 1 ++ if max_parts is not None and _parts_decoded > max_parts: ++ raise exceptions.RequestEntityTooLarge() + if is_file: + container.seek(0) + yield ( +@@ -577,7 +588,8 @@ class MultiPartParser(object): + + def parse(self, file, boundary, content_length): + formstream, filestream = tee( +- self.parse_parts(file, boundary, content_length), 2 ++ self.parse_parts(file, boundary, content_length, ++ max_parts=self.max_form_parts), 2 + ) + form = (p[1] for p in formstream if p[0] == "form") + files = (p[1] for p in filestream if p[0] == "file") +--- a/tests/test_formparser.py ++++ b/tests/test_formparser.py +@@ -124,6 +124,15 @@ class TestFormParser(object): + req.max_form_memory_size = 400 + strict_eq(req.form["foo"], u"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" +--- a/src/werkzeug/wrappers/base_request.py ++++ b/src/werkzeug/wrappers/base_request.py +@@ -98,6 +98,13 @@ class BaseRequest(object): + #: .. versionadded:: 0.5 + max_form_memory_size = 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 class to use for `args` and `form`. The default is an + #: :class:`~werkzeug.datastructures.ImmutableMultiDict` which supports + #: multiple values per key. alternatively it makes sense to use an +@@ -293,6 +300,7 @@ class BaseRequest(object): + 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): diff -Nru python-werkzeug-1.0.1+dfsg1/debian/patches/series python-werkzeug-1.0.1+dfsg1/debian/patches/series --- python-werkzeug-1.0.1+dfsg1/debian/patches/series 2020-07-14 07:35:48.000000000 +0000 +++ python-werkzeug-1.0.1+dfsg1/debian/patches/series 2023-06-14 07:47:45.000000000 +0000 @@ -1,3 +1,5 @@ drop_ubuntu_font.patch pytest-timeout.patch disable-sphinxcontrib-log_cabinet.patch +don-t-strip-leading-when-parsing-cookie.patch +limit-the-maximum-number-of-multipart-form-parts.patch