Skip to content

Commit 71b69df

Browse files
committed
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.
1 parent d2d3869 commit 71b69df

File tree

5 files changed

+50
-7
lines changed

5 files changed

+50
-7
lines changed

CHANGES.rst

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ Version 3.0.3
55

66
Unreleased
77

8+
- Only allow ``localhost``, ``.localhost``, ``127.0.0.1``, or the specified
9+
hostname when running the dev server, to make debugger requests. Additional
10+
hosts can be added by using the debugger middleware directly. The debugger
11+
UI makes requests using the full URL rather than only the path.
12+
:ghsa:`2g68-c3qc-8985`
813
- Make reloader more robust when ``""`` is in ``sys.path``. :pr:`2823`
914
- Better TLS cert format with ``adhoc`` dev certs. :pr:`2891`
1015
- Inform Python < 3.12 how to handle ``itms-services`` URIs correctly, rather

docs/debug.rst

+30-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ interactive debug console to execute code in any frame.
1616
The debugger allows the execution of arbitrary code which makes it a
1717
major security risk. **The debugger must never be used on production
1818
machines. We cannot stress this enough. Do not enable the debugger
19-
in production.**
19+
in production.** Production means anything that is not development,
20+
and anything that is publicly accessible.
2021

2122
.. note::
2223

@@ -72,10 +73,9 @@ argument to get a detailed list of all the attributes it has.
7273
Debugger PIN
7374
------------
7475

75-
Starting with Werkzeug 0.11 the debug console is protected by a PIN.
76-
This is a security helper to make it less likely for the debugger to be
77-
exploited if you forget to disable it when deploying to production. The
78-
PIN based authentication is enabled by default.
76+
The debug console is protected by a PIN. This is a security helper to make it
77+
less likely for the debugger to be exploited if you forget to disable it when
78+
deploying to production. The PIN based authentication is enabled by default.
7979

8080
The first time a console is opened, a dialog will prompt for a PIN that
8181
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.
9292
Never enable the debugger in production.**
9393

9494

95+
Allowed Hosts
96+
-------------
97+
98+
The debug console will only be served if the request comes from a trusted host.
99+
If a request comes from a browser page that is not served on a trusted URL, a
100+
400 error will be returned.
101+
102+
By default, ``localhost``, any ``.localhost`` subdomain, and ``127.0.0.1`` are
103+
trusted. ``run_simple`` will trust its ``hostname`` argument as well. To change
104+
this further, use the debug middleware directly rather than through
105+
``use_debugger=True``.
106+
107+
.. code-block:: python
108+
109+
if os.environ.get("USE_DEBUGGER") in {"1", "true"}:
110+
app = DebuggedApplication(app, evalex=True)
111+
app.trusted_hosts = [...]
112+
113+
run_simple("localhost", 8080, app)
114+
115+
**This feature is not meant to entirely secure the debugger. It is
116+
intended to make it harder for an attacker to exploit the debugger.
117+
Never enable the debugger in production.**
118+
119+
95120
Pasting Errors
96121
--------------
97122

src/werkzeug/debug/__init__.py

+10
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,14 @@ def __init__(
298298
else:
299299
self.pin = None
300300

301+
self.trusted_hosts: list[str] = [".localhost", "127.0.0.1"]
302+
"""List of domains to allow requests to the debugger from. A leading dot
303+
allows all subdomains. This only allows ``".localhost"`` domains by
304+
default.
305+
306+
.. versionadded:: 3.0.3
307+
"""
308+
301309
@property
302310
def pin(self) -> str | None:
303311
if not hasattr(self, "_pin"):
@@ -506,6 +514,8 @@ def __call__(
506514
# form data! Otherwise the application won't have access to that data
507515
# any more!
508516
request = Request(environ)
517+
request.trusted_hosts = self.trusted_hosts
518+
assert request.host # will raise 400 error if not trusted
509519
response = self.debug_application
510520
if request.args.get("__debugger__") == "yes":
511521
cmd = request.args.get("cmd")

src/werkzeug/debug/shared/debugger.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function initPinBox() {
4848
btn.disabled = true;
4949

5050
fetch(
51-
`${document.location.pathname}?__debugger__=yes&cmd=pinauth&pin=${pin}&s=${encodedSecret}`
51+
`${document.location}?__debugger__=yes&cmd=pinauth&pin=${pin}&s=${encodedSecret}`
5252
)
5353
.then((res) => res.json())
5454
.then(({auth, exhausted}) => {
@@ -79,7 +79,7 @@ function promptForPin() {
7979
if (!EVALEX_TRUSTED) {
8080
const encodedSecret = encodeURIComponent(SECRET);
8181
fetch(
82-
`${document.location.pathname}?__debugger__=yes&cmd=printpin&s=${encodedSecret}`
82+
`${document.location}?__debugger__=yes&cmd=printpin&s=${encodedSecret}`
8383
);
8484
const pinPrompt = document.getElementsByClassName("pin-prompt")[0];
8585
fadeIn(pinPrompt);

src/werkzeug/serving.py

+3
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,9 @@ def run_simple(
10721072
from .debug import DebuggedApplication
10731073

10741074
application = DebuggedApplication(application, evalex=use_evalex)
1075+
# Allow the specified hostname to use the debugger, in addition to
1076+
# localhost domains.
1077+
application.trusted_hosts.append(hostname)
10751078

10761079
if not is_running_from_reloader():
10771080
fd = None

0 commit comments

Comments
 (0)