From 71b69dfb7df3d912e66bab87fbb1f21f83504967 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 2 May 2024 11:55:52 -0700 Subject: [PATCH] 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. CVE: CVE-2024-34069 Upstream-Status: Backport [https://github.com/pallets/werkzeug/commit/71b69dfb7df3d912e66bab87fbb1f21f83504967] Signed-off-by: Soumya Sambu --- 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 49001e0..87e68c4 100644 --- a/src/werkzeug/debug/__init__.py +++ b/src/werkzeug/debug/__init__.py @@ -290,6 +290,14 @@ class DebuggedApplication: self._pin, self._pin_cookie = pin_cookie # type: ignore return self._pin + 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 + """ + @pin.setter def pin(self, value: str) -> None: self._pin = value @@ -475,6 +483,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 a19d4bd..84b0664 100644 --- a/src/werkzeug/serving.py +++ b/src/werkzeug/serving.py @@ -1038,6 +1038,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) -- 2.40.0