Skip to content

Commit

Permalink
restrict debugger trusted hosts
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
davidism committed May 5, 2024
1 parent d2d3869 commit 71b69df
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 7 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ Version 3.0.3

Unreleased

- Only allow ``localhost``, ``.localhost``, ``127.0.0.1``, or the specified
hostname when running the dev server, to make debugger requests. Additional
hosts can be added by using the debugger middleware directly. The debugger
UI makes requests using the full URL rather than only the path.
:ghsa:`2g68-c3qc-8985`
- Make reloader more robust when ``""`` is in ``sys.path``. :pr:`2823`
- Better TLS cert format with ``adhoc`` dev certs. :pr:`2891`
- Inform Python < 3.12 how to handle ``itms-services`` URIs correctly, rather
Expand Down
35 changes: 30 additions & 5 deletions docs/debug.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down Expand Up @@ -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
Expand All @@ -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
--------------

Expand Down
10 changes: 10 additions & 0 deletions src/werkzeug/debug/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,14 @@ def __init__(
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"):
Expand Down Expand Up @@ -506,6 +514,8 @@ def __call__(
# 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")
Expand Down
4 changes: 2 additions & 2 deletions src/werkzeug/debug/shared/debugger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}) => {
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/werkzeug/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,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
Expand Down

0 comments on commit 71b69df

Please sign in to comment.