145 lines
5.9 KiB
Diff
145 lines
5.9 KiB
Diff
|
|
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.
|
||
|
|
---
|
||
|
|
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 24d19bb..e779fd9 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 2a2e74d..19ed250 100644
|
||
|
|
--- a/src/werkzeug/serving.py
|
||
|
|
+++ b/src/werkzeug/serving.py
|
||
|
|
@@ -1028,6 +1028,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
|
||
|
|
--
|
||
|
|
2.41.0
|
||
|
|
|