diff -Nru python-werkzeug-0.14.1+dfsg1/debian/changelog python-werkzeug-0.14.1+dfsg1/debian/changelog --- python-werkzeug-0.14.1+dfsg1/debian/changelog 2020-11-30 13:24:10.000000000 +0000 +++ python-werkzeug-0.14.1+dfsg1/debian/changelog 2023-03-10 13:56:53.000000000 +0000 @@ -1,3 +1,16 @@ +python-werkzeug (0.14.1+dfsg1-1ubuntu0.2) bionic-security; urgency=medium + + * SECURITY UPDATE: Shadow cookie with nameless cookie + - debian/patches/CVE-2023-23934.patch: don't strip leading = when + parsing cookie. + - CVE-2023-23934 + * SECURITY UPDATE: DoS when processing unlimited multipart form data parts + - debian/patches/CVE-2023-25577.patch: limit the maximum number of + multipart form parts. + - CVE-2023-25577 + + -- Fabian Toepfer Fri, 10 Mar 2023 14:56:53 +0100 + python-werkzeug (0.14.1+dfsg1-1ubuntu0.1) bionic-security; urgency=medium * SECURITY UPDATE: Insufficient randomness in PIN diff -Nru python-werkzeug-0.14.1+dfsg1/debian/patches/CVE-2023-23934.patch python-werkzeug-0.14.1+dfsg1/debian/patches/CVE-2023-23934.patch --- python-werkzeug-0.14.1+dfsg1/debian/patches/CVE-2023-23934.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-werkzeug-0.14.1+dfsg1/debian/patches/CVE-2023-23934.patch 2023-03-10 13:56:35.000000000 +0000 @@ -0,0 +1,84 @@ +Origin: https://github.com/pallets/werkzeug/commit/cf275f42acad1b5950c50ffe8ef58fe62cdce028 +Reviewed-by: Sylvain Beucler +Last-Update: 2023-02-25 + +Backport note: the test case was modified to reflect a different value +precedence in werkzeug<1.0.0 (otherwise it passes without the patch): +-"==__Host-eq=bad;__Host-eq=good;" ++"__Host-eq=good;==__Host-eq=bad;")), + + +From 8c2b4b82d0cade0d37e6a88e2cd2413878e8ebd4 Mon Sep 17 00:00:00 2001 +From: David Lord +Date: Tue, 31 Jan 2023 14:29:34 -0800 +Subject: [PATCH] don't strip leading = when parsing cookie + +--- + 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(-) + +Index: python-werkzeug-0.14.1+dfsg1/werkzeug/_internal.py +=================================================================== +--- python-werkzeug-0.14.1+dfsg1.orig/werkzeug/_internal.py ++++ python-werkzeug-0.14.1+dfsg1/werkzeug/_internal.py +@@ -44,7 +44,7 @@ _octal_re = re.compile(br'\\[0-3][0-7][0 + _quote_re = re.compile(br'[\\].') + _legal_cookie_chars_re = br'[\w\d!#%&\'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]' + _cookie_re = re.compile(br""" +- (?P[^=;]+) ++ (?P[^=;]*) + (?:\s*=\s* + (?P + "(?:[^\\"]|\\.)*" | +@@ -277,18 +277,23 @@ 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 + + # Ignore parameters. We have no interest in them. + if key.lower() not in _cookie_params: ++ value = match.group("val") or b"" + yield _cookie_unquote(key), _cookie_unquote(value) + + +Index: python-werkzeug-0.14.1+dfsg1/tests/test_http.py +=================================================================== +--- python-werkzeug-0.14.1+dfsg1.orig/tests/test_http.py ++++ python-werkzeug-0.14.1+dfsg1/tests/test_http.py +@@ -362,13 +362,15 @@ class TestHTTPUtility(object): + def test_cookies(self): + strict_eq( + dict(http.parse_cookie('dismiss-top=6; CP=null*; PHPSESSID=0a539d42abc001cd' +- 'c762809248d4beed; a=42; b="\\\";"')), ++ 'c762809248d4beed; a=42; b="\\\";";' ++ "__Host-eq=good;==__Host-eq=bad;")), + { + 'CP': u'null*', + 'PHPSESSID': u'0a539d42abc001cdc762809248d4beed', + 'a': u'42', + 'dismiss-top': u'6', +- 'b': u'\";' ++ 'b': u'\";', ++ "__Host-eq": u"good", + } + ) + rv = http.dump_cookie('foo', 'bar baz blub', 360, httponly=True, diff -Nru python-werkzeug-0.14.1+dfsg1/debian/patches/CVE-2023-25577.patch python-werkzeug-0.14.1+dfsg1/debian/patches/CVE-2023-25577.patch --- python-werkzeug-0.14.1+dfsg1/debian/patches/CVE-2023-25577.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-werkzeug-0.14.1+dfsg1/debian/patches/CVE-2023-25577.patch 2023-03-10 13:56:41.000000000 +0000 @@ -0,0 +1,196 @@ +Origin: https://github.com/pallets/werkzeug/commit/517cac5a804e8c4dc4ed038bb20dacd038e7a9f1 +Reviewed-by: Sylvain Beucler +Last-Update: 2023-02-25 + +From fe899d0cdf767a7289a8bf746b7f72c2907a1b4b Mon Sep 17 00:00:00 2001 +From: pgjones +Date: Sun, 29 Jan 2023 15:17:37 +0000 +Subject: [PATCH 1/3] limit the maximum number of multipart form parts + +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(-) + +From 09449ee77934a0c883f5959785864ecae6aaa2c9 Mon Sep 17 00:00:00 2001 +From: David Lord +Date: Wed, 1 Feb 2023 10:31:05 -0800 +Subject: [PATCH 2/3] clean up docs + +--- + CHANGES.rst | 8 ++++---- + src/werkzeug/formparser.py | 11 +++++------ + src/werkzeug/sansio/multipart.py | 1 + + src/werkzeug/wrappers/request.py | 9 +++------ + 4 files changed, 13 insertions(+), 16 deletions(-) + +From babc8d9e8c9fa995ef26050698bc9b5a92803664 Mon Sep 17 00:00:00 2001 +From: David Lord +Date: Thu, 2 Feb 2023 09:19:15 -0800 +Subject: [PATCH 3/3] rewrite docs about request data limits + +--- + docs/request_data.rst | 37 ++++++++++++++++++++----------------- + 1 file changed, 20 insertions(+), 17 deletions(-) + +Index: python-werkzeug-0.14.1+dfsg1/werkzeug/formparser.py +=================================================================== +--- python-werkzeug-0.14.1+dfsg1.orig/werkzeug/formparser.py ++++ python-werkzeug-0.14.1+dfsg1/werkzeug/formparser.py +@@ -147,12 +147,14 @@ 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__(self, stream_factory=None, charset='utf-8', + errors='replace', max_form_memory_size=None, + max_content_length=None, cls=None, +- silent=True): ++ silent=True, max_form_parts=None): + if stream_factory is None: + stream_factory = default_stream_factory + self.stream_factory = stream_factory +@@ -160,6 +162,7 @@ class FormDataParser(object): + 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 + self.cls = cls +@@ -213,7 +216,7 @@ class FormDataParser(object): + def _parse_multipart(self, stream, mimetype, content_length, options): + parser = MultiPartParser(self.stream_factory, self.charset, self.errors, + max_form_memory_size=self.max_form_memory_size, +- cls=self.cls) ++ cls=self.cls, max_form_parts=self.max_form_parts) + boundary = options.get('boundary') + if boundary is None: + raise ValueError('Missing boundary') +@@ -295,10 +298,12 @@ _end = 'end' + class MultiPartParser(object): + + def __init__(self, stream_factory=None, charset='utf-8', errors='replace', +- max_form_memory_size=None, cls=None, buffer_size=64 * 1024): ++ 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 + self.cls = MultiDict if cls is None else cls + +@@ -479,11 +484,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: +@@ -512,6 +518,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 ('file', +@@ -525,7 +534,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') + return self.cls(form), self.cls(files) +Index: python-werkzeug-0.14.1+dfsg1/werkzeug/wrappers.py +=================================================================== +--- python-werkzeug-0.14.1+dfsg1.orig/werkzeug/wrappers.py ++++ python-werkzeug-0.14.1+dfsg1/werkzeug/wrappers.py +@@ -170,6 +170,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 +@@ -359,7 +366,8 @@ class BaseRequest(object): + self.encoding_errors, + self.max_form_memory_size, + self.max_content_length, +- self.parameter_storage_class) ++ self.parameter_storage_class, ++ max_form_parts=self.max_form_parts) + + def _load_form_data(self): + """Method used internally to retrieve submitted data. After calling +Index: python-werkzeug-0.14.1+dfsg1/tests/test_formparser.py +=================================================================== +--- python-werkzeug-0.14.1+dfsg1.orig/tests/test_formparser.py ++++ python-werkzeug-0.14.1+dfsg1/tests/test_formparser.py +@@ -13,6 +13,7 @@ from __future__ import with_statement + import pytest + + from os.path import join, dirname ++import io + + from tests import strict_eq + +@@ -101,6 +102,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' + b'Hello World\r\n' diff -Nru python-werkzeug-0.14.1+dfsg1/debian/patches/series python-werkzeug-0.14.1+dfsg1/debian/patches/series --- python-werkzeug-0.14.1+dfsg1/debian/patches/series 2020-11-30 13:14:48.000000000 +0000 +++ python-werkzeug-0.14.1+dfsg1/debian/patches/series 2023-03-10 13:56:41.000000000 +0000 @@ -1,3 +1,5 @@ drop_ubuntu_font.patch 0002-Use-local-copies-of-object.inv-for-building-document.patch CVE-2019-14806.patch +CVE-2023-23934.patch +CVE-2023-25577.patch