From 71b69dfb7df3d912e66bab87fbb1f21f83504967 Mon Sep 17 00:00:00 2001
From: David Lord <davidism@gmail.com>
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 <soumya.sambu@windriver.com>
---
 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
