diff --git a/CVE-2024-34069.patch b/CVE-2024-34069.patch new file mode 100644 index 0000000..d97ebb5 --- /dev/null +++ b/CVE-2024-34069.patch @@ -0,0 +1,226 @@ +From 71b69dfb7df3d912e66bab87fbb1f21f83504967 Mon Sep 17 00:00:00 2001 +From: David Lord +Date: Thu, 2 May 2024 11:55:52 -0700 +Subject: [PATCH 1/2] 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. +--- + CHANGES.rst | 5 ++++ + docs/debug.rst | 35 +++++++++++++++++++++++---- + src/werkzeug/debug/__init__.py | 10 ++++++++ + src/werkzeug/debug/shared/debugger.js | 4 +-- + src/werkzeug/serving.py | 3 +++ + 5 files changed, 50 insertions(+), 7 deletions(-) + +Index: werkzeug-3.0.1/docs/debug.rst +=================================================================== +--- werkzeug-3.0.1.orig/docs/debug.rst ++++ werkzeug-3.0.1/docs/debug.rst +@@ -16,7 +16,8 @@ interactive debug console to execute cod + 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 t + 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 +@@ -89,6 +89,31 @@ restarted. + + **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.** ++ ++ ++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.** + + +Index: werkzeug-3.0.1/src/werkzeug/debug/__init__.py +=================================================================== +--- werkzeug-3.0.1.orig/src/werkzeug/debug/__init__.py ++++ werkzeug-3.0.1/src/werkzeug/debug/__init__.py +@@ -19,7 +19,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 +@@ -297,6 +299,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) -> str | None: + if not hasattr(self, "_pin"): +@@ -343,7 +353,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, + ) +@@ -371,6 +381,9 @@ class DebuggedApplication: + frame: 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: +@@ -381,6 +394,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 = {} +@@ -433,12 +449,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) +@@ -488,8 +510,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:" +@@ -516,7 +541,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 +Index: werkzeug-3.0.1/src/werkzeug/debug/shared/debugger.js +=================================================================== +--- werkzeug-3.0.1.orig/src/werkzeug/debug/shared/debugger.js ++++ werkzeug-3.0.1/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); +Index: werkzeug-3.0.1/src/werkzeug/serving.py +=================================================================== +--- werkzeug-3.0.1.orig/src/werkzeug/serving.py ++++ werkzeug-3.0.1/src/werkzeug/serving.py +@@ -1068,6 +1068,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(): + fd = None +Index: werkzeug-3.0.1/src/werkzeug/sansio/utils.py +=================================================================== +--- werkzeug-3.0.1.orig/src/werkzeug/sansio/utils.py ++++ werkzeug-3.0.1/src/werkzeug/sansio/utils.py +@@ -8,7 +8,7 @@ from ..exceptions import SecurityError + from ..urls import uri_to_iri + + +-def host_is_trusted(hostname: str, trusted_list: t.Iterable[str]) -> bool: ++def host_is_trusted(hostname: str | None, trusted_list: t.Iterable[str]) -> bool: + """Check if a host matches a list of trusted names. + + :param hostname: The name to check. diff --git a/python-Werkzeug.changes b/python-Werkzeug.changes index 0ba1612..c30b282 100644 --- a/python-Werkzeug.changes +++ b/python-Werkzeug.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Mon Jun 3 06:54:48 UTC 2024 - Daniel Garcia + +- Add CVE-2024-34069.patch, restrict debugger trusted hosts. + (bsc#1223979, CVE-2024-34069, gh#pallets/werkzeug@3386395b24c7) + ------------------------------------------------------------------- Fri Oct 27 03:06:50 UTC 2023 - Steve Kowalik diff --git a/python-Werkzeug.spec b/python-Werkzeug.spec index 28233ec..d419b13 100644 --- a/python-Werkzeug.spec +++ b/python-Werkzeug.spec @@ -33,6 +33,8 @@ Summary: The Swiss Army knife of Python web development License: BSD-3-Clause URL: https://werkzeug.palletsprojects.com Source: https://files.pythonhosted.org/packages/source/w/werkzeug/werkzeug-%{version}.tar.gz +# PATCH-FIX-UPSTREAM CVE-2024-34069.patch gh#pallets/werkzeug@3386395b24c7 +Patch0: CVE-2024-34069.patch BuildRequires: %{python_module base >= 3.8} BuildRequires: %{python_module flit-core} BuildRequires: %{python_module pip}