VYPR
High severityNVD Advisory· Published May 6, 2024· Updated Feb 21, 2025

Werkzeug's improper usage of a pathname and improper CSRF protection results in the remote command execution

CVE-2024-34069

Description

Werkzeug is a comprehensive WSGI web application library. The debugger in affected versions of Werkzeug can allow an attacker to execute code on a developer's machine under some circumstances. This requires the attacker to get the developer to interact with a domain and subdomain they control, and enter the debugger PIN, but if they are successful it allows access to the debugger even if it is only running on localhost. This also requires the attacker to guess a URL in the developer's application that will trigger the debugger. This vulnerability is fixed in 3.0.3.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
WerkzeugPyPI
< 3.0.33.0.3

Affected products

1

Patches

1
3386395b24c7

Merge pull request from GHSA-2g68-c3qc-8985

https://github.com/pallets/werkzeugDavid LordMay 5, 2024via ghsa
6 files changed · +69 11
  • CHANGES.rst+5 0 modified
    @@ -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
    
  • docs/debug.rst+30 5 modified
    @@ -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
     --------------
     
    
  • src/werkzeug/debug/__init__.py+28 3 modified
    @@ -19,7 +19,9 @@
     
     from .._internal import _log
     from ..exceptions import NotFound
    +from ..exceptions import SecurityError
     from ..http import parse_cookie
    +from ..sansio.utils import host_is_trusted
     from ..security import gen_salt
     from ..utils import send_file
     from ..wrappers.request import Request
    @@ -298,6 +300,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"):
    @@ -344,7 +354,7 @@ def debug_application(
     
                 is_trusted = bool(self.check_pin_trust(environ))
                 html = tb.render_debugger_html(
    -                evalex=self.evalex,
    +                evalex=self.evalex and self.check_host_trust(environ),
                     secret=self.secret,
                     evalex_trusted=is_trusted,
                 )
    @@ -372,6 +382,9 @@ def execute_command(  # type: ignore[return]
             frame: DebugFrameSummary | _ConsoleFrame,
         ) -> Response:
             """Execute a command in a console."""
    +        if not self.check_host_trust(request.environ):
    +            return SecurityError()  # type: ignore[return-value]
    +
             contexts = self.frame_contexts.get(id(frame), [])
     
             with ExitStack() as exit_stack:
    @@ -382,6 +395,9 @@ def execute_command(  # type: ignore[return]
     
         def display_console(self, request: Request) -> Response:
             """Display a standalone shell."""
    +        if not self.check_host_trust(request.environ):
    +            return SecurityError()  # type: ignore[return-value]
    +
             if 0 not in self.frames:
                 if self.console_init_func is None:
                     ns = {}
    @@ -434,12 +450,18 @@ def check_pin_trust(self, environ: WSGIEnvironment) -> bool | None:
                 return None
             return (time.time() - PIN_TIME) < ts
     
    +    def check_host_trust(self, environ: WSGIEnvironment) -> bool:
    +        return host_is_trusted(environ.get("HTTP_HOST"), self.trusted_hosts)
    +
         def _fail_pin_auth(self) -> None:
             time.sleep(5.0 if self._failed_pin_auth > 5 else 0.5)
             self._failed_pin_auth += 1
     
         def pin_auth(self, request: Request) -> Response:
             """Authenticates with the pin."""
    +        if not self.check_host_trust(request.environ):
    +            return SecurityError()  # type: ignore[return-value]
    +
             exhausted = False
             auth = False
             trust = self.check_pin_trust(request.environ)
    @@ -489,8 +511,11 @@ def pin_auth(self, request: Request) -> Response:
                 rv.delete_cookie(self.pin_cookie_name)
             return rv
     
    -    def log_pin_request(self) -> Response:
    +    def log_pin_request(self, request: Request) -> Response:
             """Log the pin if needed."""
    +        if not self.check_host_trust(request.environ):
    +            return SecurityError()  # type: ignore[return-value]
    +
             if self.pin_logging and self.pin is not None:
                 _log(
                     "info", " * To enable the debugger you need to enter the security pin:"
    @@ -517,7 +542,7 @@ def __call__(
                 elif cmd == "pinauth" and secret == self.secret:
                     response = self.pin_auth(request)  # type: ignore
                 elif cmd == "printpin" and secret == self.secret:
    -                response = self.log_pin_request()  # type: ignore
    +                response = self.log_pin_request(request)  # type: ignore
                 elif (
                     self.evalex
                     and cmd is not None
    
  • src/werkzeug/debug/shared/debugger.js+2 2 modified
    @@ -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);
    
  • src/werkzeug/sansio/utils.py+1 1 modified
    @@ -8,7 +8,7 @@
     from ..urls import uri_to_iri
     
     
    -def host_is_trusted(hostname: str, trusted_list: t.Iterable[str]) -> bool:
    +def host_is_trusted(hostname: str | None, trusted_list: t.Iterable[str]) -> bool:
         """Check if a host matches a list of trusted names.
     
         :param hostname: The name to check.
    
  • src/werkzeug/serving.py+3 0 modified
    @@ -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
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

11

News mentions

0

No linked articles in our index yet.