Package: release.debian.org
Severity: normal
Tags: bookworm
User: release.debian....@packages.debian.org
Usertags: pu
X-Debbugs-Cc: debian-pyt...@lists.debian.org, c.schoen...@t-online.de
Control: affects -1 + src:python-werkzeug

Dear release team,

Fix three DoS CVEs.  Straightforward backports of upstream's commits, all of
which are already in unstable.

Upstream's test suite passes so I believe that no ordinary functionality is
impacted, although, upstream did not provide new tests to verify the fixes.

The changes regarding trusted hosts is only for the debugger, so wouldn't
inadvertedly impact anyone's production usage.

I have not uploaded, but pushed to 
salsa:python-team/packages/python-werkzeug#debian/bookworm.

Thanks.

-- 
Sean Whitton
diff -Nru python-werkzeug-2.2.2/debian/changelog 
python-werkzeug-2.2.2/debian/changelog
--- python-werkzeug-2.2.2/debian/changelog      2023-04-21 19:37:22.000000000 
+0800
+++ python-werkzeug-2.2.2/debian/changelog      2024-12-05 11:47:13.000000000 
+0800
@@ -1,3 +1,11 @@
+python-werkzeug (2.2.2-3+deb12u1) bookworm; urgency=high
+
+  * Backport upstream fix for CVE-2023-46136 (Closes: #1054553).
+  * Backport upstream fixes for CVE-2024-34069 (Closes: #1070711).
+  * Backport upstream fix for CVE-2024-49767 (Closes: #1086062).
+
+ -- Sean Whitton <spwhit...@spwhitton.name>  Thu, 05 Dec 2024 11:47:13 +0800
+
 python-werkzeug (2.2.2-3) unstable; urgency=medium
 
   [ Robin Gustafsson ]
diff -Nru python-werkzeug-2.2.2/debian/patches/CVE-2023-46136.patch 
python-werkzeug-2.2.2/debian/patches/CVE-2023-46136.patch
--- python-werkzeug-2.2.2/debian/patches/CVE-2023-46136.patch   1970-01-01 
08:00:00.000000000 +0800
+++ python-werkzeug-2.2.2/debian/patches/CVE-2023-46136.patch   2024-12-05 
11:47:13.000000000 +0800
@@ -0,0 +1,35 @@
+From: =?utf-8?q?Pawe=C5=82_Srokosz?= <pawel.srok...@cert.pl>
+Date: Thu, 12 Oct 2023 18:50:04 +0200
+Subject: Fix: slow multipart parsing for huge files with few CR/LF characters
+
+(cherry picked from commit b1916c0c083e0be1c9d887ee2f3d696922bfc5c1)
+---
+ src/werkzeug/sansio/multipart.py | 10 +++++++++-
+ 1 file changed, 9 insertions(+), 1 deletion(-)
+
+diff --git a/src/werkzeug/sansio/multipart.py 
b/src/werkzeug/sansio/multipart.py
+index 2684e5d..2c0947d 100644
+--- a/src/werkzeug/sansio/multipart.py
++++ b/src/werkzeug/sansio/multipart.py
+@@ -206,12 +206,20 @@ class MultipartDecoder:
+                 self._search_position = max(0, len(self.buffer) - 
SEARCH_EXTRA_LENGTH)
+ 
+         elif self.state == State.DATA:
+-            if self.buffer.find(b"--" + self.boundary) == -1:
++            boundary = b"--" + self.boundary
++
++            if self.buffer.find(boundary) == -1:
+                 # No complete boundary in the buffer, but there may be
+                 # a partial boundary at the end. As the boundary
+                 # starts with either a nl or cr find the earliest and
+                 # return up to that as data.
+                 data_length = del_index = self.last_newline()
++                # If amount of data after last newline is far from
++                # possible length of partial boundary, we should
++                # assume that there is no partial boundary in the buffer
++                # and return all pending data.
++                if (len(self.buffer) - data_length) > len(b"\n" + boundary):
++                    data_length = del_index = len(self.buffer)
+                 more_data = True
+             else:
+                 match = self.boundary_re.search(self.buffer)
diff -Nru python-werkzeug-2.2.2/debian/patches/CVE-2024-34069-1.patch 
python-werkzeug-2.2.2/debian/patches/CVE-2024-34069-1.patch
--- python-werkzeug-2.2.2/debian/patches/CVE-2024-34069-1.patch 1970-01-01 
08:00:00.000000000 +0800
+++ python-werkzeug-2.2.2/debian/patches/CVE-2024-34069-1.patch 2024-12-05 
11:47:13.000000000 +0800
@@ -0,0 +1,142 @@
+From: David Lord <david...@gmail.com>
+Date: Thu, 2 May 2024 11:55:52 -0700
+Subject: restrict debugger trusted hosts
+
+Add a list of `trusted_hosts` to the `DebuggedApplication` middleware. It 
defaults to only allowing `localhost`, `.localhost` subdomains, and 
`127.0.0.1`. `run_simple(use_debugger=True)` adds its `hostname` argument to 
the trusted list as well. The middleware can be used directly to further modify 
the trusted list in less common development scenarios.
+
+The debugger UI uses the full `document.location` instead of only 
`document.location.pathname`.
+
+Either of these fixes on their own mitigates the reported vulnerability.
+
+(cherry picked from commit 71b69dfb7df3d912e66bab87fbb1f21f83504967)
+---
+ docs/debug.rst                        | 35 ++++++++++++++++++++++++++++++-----
+ src/werkzeug/debug/__init__.py        | 10 ++++++++++
+ src/werkzeug/debug/shared/debugger.js |  4 ++--
+ src/werkzeug/serving.py               |  3 +++
+ 4 files changed, 45 insertions(+), 7 deletions(-)
+
+diff --git a/docs/debug.rst b/docs/debug.rst
+index 25a9f0b..d842135 100644
+--- a/docs/debug.rst
++++ b/docs/debug.rst
+@@ -16,7 +16,8 @@ interactive debug console to execute code in any frame.
+     The debugger allows the execution of arbitrary code which makes it a
+     major security risk. **The debugger must never be used on production
+     machines. We cannot stress this enough. Do not enable the debugger
+-    in production.**
++    in production.** Production means anything that is not development,
++    and anything that is publicly accessible.
+ 
+ .. note::
+ 
+@@ -72,10 +73,9 @@ argument to get a detailed list of all the attributes it 
has.
+ Debugger PIN
+ ------------
+ 
+-Starting with Werkzeug 0.11 the debug console is protected by a PIN.
+-This is a security helper to make it less likely for the debugger to be
+-exploited if you forget to disable it when deploying to production. The
+-PIN based authentication is enabled by default.
++The debug console is protected by a PIN. This is a security helper to make it
++less likely for the debugger to be exploited if you forget to disable it when
++deploying to production. The PIN based authentication is enabled by default.
+ 
+ The first time a console is opened, a dialog will prompt for a PIN that
+ is printed to the command line. The PIN is generated in a stable way
+@@ -92,6 +92,31 @@ intended to make it harder for an attacker to exploit the 
debugger.
+ Never enable the debugger in production.**
+ 
+ 
++Allowed Hosts
++-------------
++
++The debug console will only be served if the request comes from a trusted 
host.
++If a request comes from a browser page that is not served on a trusted URL, a
++400 error will be returned.
++
++By default, ``localhost``, any ``.localhost`` subdomain, and ``127.0.0.1`` are
++trusted. ``run_simple`` will trust its ``hostname`` argument as well. To 
change
++this further, use the debug middleware directly rather than through
++``use_debugger=True``.
++
++.. code-block:: python
++
++    if os.environ.get("USE_DEBUGGER") in {"1", "true"}:
++        app = DebuggedApplication(app, evalex=True)
++        app.trusted_hosts = [...]
++
++    run_simple("localhost", 8080, app)
++
++**This feature is not meant to entirely secure the debugger. It is
++intended to make it harder for an attacker to exploit the debugger.
++Never enable the debugger in production.**
++
++
+ Pasting Errors
+ --------------
+ 
+diff --git a/src/werkzeug/debug/__init__.py b/src/werkzeug/debug/__init__.py
+index e0dcc65..9579f2c 100644
+--- a/src/werkzeug/debug/__init__.py
++++ b/src/werkzeug/debug/__init__.py
+@@ -296,6 +296,14 @@ class DebuggedApplication:
+         else:
+             self.pin = None
+ 
++        self.trusted_hosts: list[str] = [".localhost", "127.0.0.1"]
++        """List of domains to allow requests to the debugger from. A leading 
dot
++        allows all subdomains. This only allows ``".localhost"`` domains by
++        default.
++
++        .. versionadded:: 3.0.3
++        """
++
+     @property
+     def pin(self) -> t.Optional[str]:
+         if not hasattr(self, "_pin"):
+@@ -504,6 +512,8 @@ class DebuggedApplication:
+         # form data!  Otherwise the application won't have access to that data
+         # any more!
+         request = Request(environ)
++        request.trusted_hosts = self.trusted_hosts
++        assert request.host  # will raise 400 error if not trusted
+         response = self.debug_application
+         if request.args.get("__debugger__") == "yes":
+             cmd = request.args.get("cmd")
+diff --git a/src/werkzeug/debug/shared/debugger.js 
b/src/werkzeug/debug/shared/debugger.js
+index 2354f03..bee079f 100644
+--- a/src/werkzeug/debug/shared/debugger.js
++++ b/src/werkzeug/debug/shared/debugger.js
+@@ -48,7 +48,7 @@ function initPinBox() {
+       btn.disabled = true;
+ 
+       fetch(
+-        
`${document.location.pathname}?__debugger__=yes&cmd=pinauth&pin=${pin}&s=${encodedSecret}`
++        
`${document.location}?__debugger__=yes&cmd=pinauth&pin=${pin}&s=${encodedSecret}`
+       )
+         .then((res) => res.json())
+         .then(({auth, exhausted}) => {
+@@ -79,7 +79,7 @@ function promptForPin() {
+   if (!EVALEX_TRUSTED) {
+     const encodedSecret = encodeURIComponent(SECRET);
+     fetch(
+-      
`${document.location.pathname}?__debugger__=yes&cmd=printpin&s=${encodedSecret}`
++      `${document.location}?__debugger__=yes&cmd=printpin&s=${encodedSecret}`
+     );
+     const pinPrompt = document.getElementsByClassName("pin-prompt")[0];
+     fadeIn(pinPrompt);
+diff --git a/src/werkzeug/serving.py b/src/werkzeug/serving.py
+index c482469..2db07be 100644
+--- a/src/werkzeug/serving.py
++++ b/src/werkzeug/serving.py
+@@ -1057,6 +1057,9 @@ def run_simple(
+         from .debug import DebuggedApplication
+ 
+         application = DebuggedApplication(application, evalex=use_evalex)
++        # Allow the specified hostname to use the debugger, in addition to
++        # localhost domains.
++        application.trusted_hosts.append(hostname)
+ 
+     if not is_running_from_reloader():
+         s = prepare_socket(hostname, port)
diff -Nru python-werkzeug-2.2.2/debian/patches/CVE-2024-34069-2.patch 
python-werkzeug-2.2.2/debian/patches/CVE-2024-34069-2.patch
--- python-werkzeug-2.2.2/debian/patches/CVE-2024-34069-2.patch 1970-01-01 
08:00:00.000000000 +0800
+++ python-werkzeug-2.2.2/debian/patches/CVE-2024-34069-2.patch 2024-12-05 
11:47:13.000000000 +0800
@@ -0,0 +1,116 @@
+From: David Lord <david...@gmail.com>
+Date: Fri, 3 May 2024 14:49:43 -0700
+Subject: only require trusted host for evalex
+
+(cherry picked from commit 890b6b62634fa61224222aee31081c61b054ff01)
+---
+ src/werkzeug/debug/__init__.py | 25 ++++++++++++++++++++-----
+ src/werkzeug/sansio/utils.py   |  2 +-
+ 2 files changed, 21 insertions(+), 6 deletions(-)
+
+diff --git a/src/werkzeug/debug/__init__.py b/src/werkzeug/debug/__init__.py
+index 9579f2c..1bba5c0 100644
+--- a/src/werkzeug/debug/__init__.py
++++ b/src/werkzeug/debug/__init__.py
+@@ -18,7 +18,9 @@ from zlib import adler32
+ 
+ from .._internal import _log
+ from ..exceptions import NotFound
++from ..exceptions import SecurityError
+ from ..http import parse_cookie
++from ..sansio.utils import host_is_trusted
+ from ..security import gen_salt
+ from ..utils import send_file
+ from ..wrappers.request import Request
+@@ -350,7 +352,7 @@ class DebuggedApplication:
+ 
+             is_trusted = bool(self.check_pin_trust(environ))
+             html = tb.render_debugger_html(
+-                evalex=self.evalex,
++                evalex=self.evalex and self.check_host_trust(environ),
+                 secret=self.secret,
+                 evalex_trusted=is_trusted,
+             )
+@@ -378,6 +380,9 @@ class DebuggedApplication:
+         frame: t.Union[DebugFrameSummary, _ConsoleFrame],
+     ) -> Response:
+         """Execute a command in a console."""
++        if not self.check_host_trust(request.environ):
++            return SecurityError()  # type: ignore[return-value]
++
+         contexts = self.frame_contexts.get(id(frame), [])
+ 
+         with ExitStack() as exit_stack:
+@@ -388,6 +393,9 @@ class DebuggedApplication:
+ 
+     def display_console(self, request: Request) -> Response:
+         """Display a standalone shell."""
++        if not self.check_host_trust(request.environ):
++            return SecurityError()  # type: ignore[return-value]
++
+         if 0 not in self.frames:
+             if self.console_init_func is None:
+                 ns = {}
+@@ -440,12 +448,18 @@ class DebuggedApplication:
+             return None
+         return (time.time() - PIN_TIME) < ts
+ 
++    def check_host_trust(self, environ: "WSGIEnvironment") -> bool:
++        return host_is_trusted(environ.get("HTTP_HOST"), self.trusted_hosts)
++
+     def _fail_pin_auth(self) -> None:
+         time.sleep(5.0 if self._failed_pin_auth > 5 else 0.5)
+         self._failed_pin_auth += 1
+ 
+     def pin_auth(self, request: Request) -> Response:
+         """Authenticates with the pin."""
++        if not self.check_host_trust(request.environ):
++            return SecurityError()  # type: ignore[return-value]
++
+         exhausted = False
+         auth = False
+         trust = self.check_pin_trust(request.environ)
+@@ -495,8 +509,11 @@ class DebuggedApplication:
+             rv.delete_cookie(self.pin_cookie_name)
+         return rv
+ 
+-    def log_pin_request(self) -> Response:
++    def log_pin_request(self, request: Request) -> Response:
+         """Log the pin if needed."""
++        if not self.check_host_trust(request.environ):
++            return SecurityError()  # type: ignore[return-value]
++
+         if self.pin_logging and self.pin is not None:
+             _log(
+                 "info", " * To enable the debugger you need to enter the 
security pin:"
+@@ -512,8 +529,6 @@ class DebuggedApplication:
+         # form data!  Otherwise the application won't have access to that data
+         # any more!
+         request = Request(environ)
+-        request.trusted_hosts = self.trusted_hosts
+-        assert request.host  # will raise 400 error if not trusted
+         response = self.debug_application
+         if request.args.get("__debugger__") == "yes":
+             cmd = request.args.get("cmd")
+@@ -525,7 +540,7 @@ class DebuggedApplication:
+             elif cmd == "pinauth" and secret == self.secret:
+                 response = self.pin_auth(request)  # type: ignore
+             elif cmd == "printpin" and secret == self.secret:
+-                response = self.log_pin_request()  # type: ignore
++                response = self.log_pin_request(request)  # type: ignore
+             elif (
+                 self.evalex
+                 and cmd is not None
+diff --git a/src/werkzeug/sansio/utils.py b/src/werkzeug/sansio/utils.py
+index e639dcb..cc85927 100644
+--- a/src/werkzeug/sansio/utils.py
++++ b/src/werkzeug/sansio/utils.py
+@@ -6,7 +6,7 @@ from ..urls import uri_to_iri
+ from ..urls import url_quote
+ 
+ 
+-def host_is_trusted(hostname: str, trusted_list: t.Iterable[str]) -> bool:
++def host_is_trusted(hostname: t.Optional[str], trusted_list: t.Iterable[str]) 
-> bool:
+     """Check if a host matches a list of trusted names.
+ 
+     :param hostname: The name to check.
diff -Nru python-werkzeug-2.2.2/debian/patches/CVE-2024-49767.patch 
python-werkzeug-2.2.2/debian/patches/CVE-2024-49767.patch
--- python-werkzeug-2.2.2/debian/patches/CVE-2024-49767.patch   1970-01-01 
08:00:00.000000000 +0800
+++ python-werkzeug-2.2.2/debian/patches/CVE-2024-49767.patch   2024-12-05 
11:47:13.000000000 +0800
@@ -0,0 +1,80 @@
+From: David Lord <david...@gmail.com>
+Date: Fri, 25 Oct 2024 06:46:50 -0700
+Subject: apply max_form_memory_size another level up in the parser
+
+(cherry picked from commit 8760275afb72bd10b57d92cb4d52abf759b2f3a7)
+---
+ src/werkzeug/formparser.py       | 11 +++++++++++
+ src/werkzeug/sansio/multipart.py |  2 ++
+ tests/test_formparser.py         | 12 ++++++++++++
+ 3 files changed, 25 insertions(+)
+
+diff --git a/src/werkzeug/formparser.py b/src/werkzeug/formparser.py
+index bebb2fc..fc458cd 100644
+--- a/src/werkzeug/formparser.py
++++ b/src/werkzeug/formparser.py
+@@ -405,6 +405,7 @@ class MultiPartParser:
+     def parse(
+         self, stream: t.IO[bytes], boundary: bytes, content_length: 
t.Optional[int]
+     ) -> t.Tuple[MultiDict, MultiDict]:
++        field_size: t.Optional[int] = None
+         container: t.Union[t.IO[bytes], t.List[bytes]]
+         _write: t.Callable[[bytes], t.Any]
+ 
+@@ -431,13 +432,23 @@ class MultiPartParser:
+             while not isinstance(event, (Epilogue, NeedData)):
+                 if isinstance(event, Field):
+                     current_part = event
++                    field_size = 0
+                     container = []
+                     _write = container.append
+                 elif isinstance(event, File):
+                     current_part = event
++                    field_size = None
+                     container = self.start_file_streaming(event, 
content_length)
+                     _write = container.write
+                 elif isinstance(event, Data):
++                    if self.max_form_memory_size is not None and field_size 
is not None:
++                        # Ensure that accumulated data events do not exceed 
limit.
++                        # Also checked within single event in 
MultipartDecoder.
++                        field_size += len(event.data)
++
++                        if field_size > self.max_form_memory_size:
++                            raise RequestEntityTooLarge()
++
+                     _write(event.data)
+                     if not event.more_data:
+                         if isinstance(current_part, Field):
+diff --git a/src/werkzeug/sansio/multipart.py 
b/src/werkzeug/sansio/multipart.py
+index 2c0947d..99eb6ea 100644
+--- a/src/werkzeug/sansio/multipart.py
++++ b/src/werkzeug/sansio/multipart.py
+@@ -142,6 +142,8 @@ class MultipartDecoder:
+             self.max_form_memory_size is not None
+             and len(self.buffer) + len(data) > self.max_form_memory_size
+         ):
++            # Ensure that data within single event does not exceed limit.
++            # Also checked across accumulated events in MultiPartParser.
+             raise RequestEntityTooLarge()
+         else:
+             self.buffer.extend(data)
+diff --git a/tests/test_formparser.py b/tests/test_formparser.py
+index 4c518b1..05fa84e 100644
+--- a/tests/test_formparser.py
++++ b/tests/test_formparser.py
+@@ -455,3 +455,15 @@ class TestMultiPartParser:
+         ) as request:
+             assert request.files["rfc2231"].filename == "a b c d e f.txt"
+             assert request.files["rfc2231"].read() == b"file contents"
++
++
++def test_multipart_max_form_memory_size() -> None:
++    """max_form_memory_size is tracked across multiple data events."""
++    data = b"--bound\r\nContent-Disposition: form-field; name=a\r\n\r\n"
++    data += b"a" * 15 + b"\r\n--bound--"
++    # The buffer size is less than the max size, so multiple data events will 
be
++    # returned. The field size is greater than the max.
++    parser = formparser.MultiPartParser(max_form_memory_size=10, 
buffer_size=5)
++
++    with pytest.raises(RequestEntityTooLarge):
++        parser.parse(io.BytesIO(data), b"bound", None)
diff -Nru python-werkzeug-2.2.2/debian/patches/series 
python-werkzeug-2.2.2/debian/patches/series
--- python-werkzeug-2.2.2/debian/patches/series 2023-04-21 19:37:22.000000000 
+0800
+++ python-werkzeug-2.2.2/debian/patches/series 2024-12-05 11:47:13.000000000 
+0800
@@ -2,3 +2,7 @@
 remove-test_exclude_patterns-test.patch
 0003-don-t-strip-leading-when-parsing-cookie.patch
 0004-limit-the-maximum-number-of-multipart-form-parts.patch
+CVE-2023-46136.patch
+CVE-2024-34069-1.patch
+CVE-2024-34069-2.patch
+CVE-2024-49767.patch

Attachment: signature.asc
Description: PGP signature

Reply via email to