From e633b30cd1d9bcab5692f422e71084cc4f27ebde Mon Sep 17 00:00:00 2001 From: momotarogrp <75789359+momotarogrp@users.noreply.github.com> Date: Thu, 28 Dec 2023 02:16:50 +0900 Subject: [PATCH 1/7] Update _reloader.py root={} path=() os.path.join(*path) Traceback (most recent call last): File "", line 1, in TypeError: join() missing 1 required positional argument: 'path' --- CHANGES.rst | 2 ++ src/werkzeug/_reloader.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index b3fc39b27..05e5acf29 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,8 @@ Version 3.0.3 Unreleased +- Make reloader more robust when ``""`` is in ``sys.path``. :pr:`2823` + Version 3.0.2 ------------- diff --git a/src/werkzeug/_reloader.py b/src/werkzeug/_reloader.py index 24c2dab79..d7e91a61c 100644 --- a/src/werkzeug/_reloader.py +++ b/src/werkzeug/_reloader.py @@ -157,7 +157,9 @@ def _walk(node: t.Mapping[str, dict[str, t.Any]], path: tuple[str, ...]) -> None for prefix, child in node.items(): _walk(child, path + (prefix,)) - if not node: + # If there are no more nodes, and a path has been accumulated, add it. + # Path may be empty if the "" entry is in sys.path. + if not node and path: rv.add(os.path.join(*path)) _walk(root, ()) From 793be472c9d145eb9be7d4200672d1806289d84a Mon Sep 17 00:00:00 2001 From: afdy Date: Thu, 2 May 2024 08:43:15 +0100 Subject: [PATCH 2/7] update adhoc tls dev cert format single host in cn field san extension for wildcard name --- CHANGES.rst | 1 + src/werkzeug/serving.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 05e5acf29..279fd3f7d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Version 3.0.3 Unreleased - Make reloader more robust when ``""`` is in ``sys.path``. :pr:`2823` +- Better TLS cert format with ``adhoc`` dev certs. :pr:`2891` Version 3.0.2 diff --git a/src/werkzeug/serving.py b/src/werkzeug/serving.py index ad6bf911b..7f6ea922e 100644 --- a/src/werkzeug/serving.py +++ b/src/werkzeug/serving.py @@ -532,7 +532,10 @@ def generate_adhoc_ssl_pair( .not_valid_before(dt.now(timezone.utc)) .not_valid_after(dt.now(timezone.utc) + timedelta(days=365)) .add_extension(x509.ExtendedKeyUsage([x509.OID_SERVER_AUTH]), critical=False) - .add_extension(x509.SubjectAlternativeName([x509.DNSName(cn)]), critical=False) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName(cn), x509.DNSName(f"*.{cn}")]), + critical=False, + ) .sign(pkey, hashes.SHA256(), backend) ) return cert, pkey @@ -560,7 +563,7 @@ def make_ssl_devcert( """ if host is not None: - cn = f"*.{host}/CN={host}" + cn = host cert, pkey = generate_adhoc_ssl_pair(cn=cn) from cryptography.hazmat.primitives import serialization From 97fb2f722297ae4e12e36dab024e0acf8477b3c8 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 5 May 2024 08:55:42 -0700 Subject: [PATCH 3/7] remove _invalid_iri_to_uri workaround tell Python to handle itms-services scheme correctly --- CHANGES.rst | 3 +++ src/werkzeug/urls.py | 25 ++++++------------------- src/werkzeug/wrappers/response.py | 3 +-- tests/test_urls.py | 6 ++++++ tests/test_wrappers.py | 1 + 5 files changed, 17 insertions(+), 21 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 279fd3f7d..367cfb668 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,9 @@ Unreleased - 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 + than using an overly-broad workaround in Werkzeug that caused some redirect + URIs to be passed on without encoding. :issue:`2828` Version 3.0.2 diff --git a/src/werkzeug/urls.py b/src/werkzeug/urls.py index 4d61e600b..5bffe3928 100644 --- a/src/werkzeug/urls.py +++ b/src/werkzeug/urls.py @@ -3,6 +3,7 @@ import codecs import re import typing as t +import urllib.parse from urllib.parse import quote from urllib.parse import unquote from urllib.parse import urlencode @@ -164,25 +165,11 @@ def iri_to_uri(iri: str) -> str: return urlunsplit((parts.scheme, netloc, path, query, fragment)) -def _invalid_iri_to_uri(iri: str) -> str: - """The URL scheme ``itms-services://`` must contain the ``//`` even though it does - not have a host component. There may be other invalid schemes as well. Currently, - responses will always call ``iri_to_uri`` on the redirect ``Location`` header, which - removes the ``//``. For now, if the IRI only contains ASCII and does not contain - spaces, pass it on as-is. In Werkzeug 3.0, this should become a - ``response.process_location`` flag. - - :meta private: - """ - try: - iri.encode("ascii") - except UnicodeError: - pass - else: - if len(iri.split(None, 1)) == 1: - return iri - - return iri_to_uri(iri) +# Python < 3.12 +# itms-services was worked around in previous iri_to_uri implementations, but +# we can tell Python directly that it needs to preserve the //. +if "itms-services" not in urllib.parse.uses_netloc: + urllib.parse.uses_netloc.append("itms-services") def _decode_idna(domain: str) -> str: diff --git a/src/werkzeug/wrappers/response.py b/src/werkzeug/wrappers/response.py index 7b666e3e8..7f01287c7 100644 --- a/src/werkzeug/wrappers/response.py +++ b/src/werkzeug/wrappers/response.py @@ -14,7 +14,6 @@ from ..http import parse_range_header from ..http import remove_entity_headers from ..sansio.response import Response as _SansIOResponse -from ..urls import _invalid_iri_to_uri from ..urls import iri_to_uri from ..utils import cached_property from ..wsgi import _RangeWrapper @@ -479,7 +478,7 @@ def get_wsgi_headers(self, environ: WSGIEnvironment) -> Headers: content_length = value if location is not None: - location = _invalid_iri_to_uri(location) + location = iri_to_uri(location) if self.autocorrect_location_header: # Make the location header an absolute URL. diff --git a/tests/test_urls.py b/tests/test_urls.py index fdaa913a6..101b886ec 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -98,3 +98,9 @@ def test_iri_to_uri_dont_quote_valid_code_points(): # [] are not valid URL code points according to WhatWG URL Standard # https://url.spec.whatwg.org/#url-code-points assert urls.iri_to_uri("/path[bracket]?(paren)") == "/path%5Bbracket%5D?(paren)" + + +# Python < 3.12 +def test_itms_services() -> None: + url = "itms-services://?action=download-manifest&url=https://test.example/path" + assert urls.iri_to_uri(url) == url diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index d7bc12b95..f75694459 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -1154,6 +1154,7 @@ class MyResponse(wrappers.Response): ("auto", "location", "expect"), ( (False, "/test", "/test"), + (False, "/\\\\test.example?q", "/%5C%5Ctest.example?q"), (True, "/test", "http://localhost/test"), (True, "test", "http://localhost/a/b/test"), (True, "./test", "http://localhost/a/b/test"), From 7080b55acd48b68afdda65ee6c7f99e9afafb0ba Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 5 May 2024 09:23:06 -0700 Subject: [PATCH 4/7] endpoint type is Any --- CHANGES.rst | 2 ++ src/werkzeug/routing/exceptions.py | 9 ++++++--- src/werkzeug/routing/map.py | 18 +++++++++--------- src/werkzeug/routing/rules.py | 4 ++-- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 367cfb668..5658080c3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,8 @@ Unreleased - Inform Python < 3.12 how to handle ``itms-services`` URIs correctly, rather than using an overly-broad workaround in Werkzeug that caused some redirect URIs to be passed on without encoding. :issue:`2828` +- Type annotation for ``Rule.endpoint`` and other uses of ``endpoint`` is + ``Any``. :issue:`2836` Version 3.0.2 diff --git a/src/werkzeug/routing/exceptions.py b/src/werkzeug/routing/exceptions.py index b63fe5b9c..eeabd4ed1 100644 --- a/src/werkzeug/routing/exceptions.py +++ b/src/werkzeug/routing/exceptions.py @@ -59,7 +59,7 @@ def __init__(self, path_info: str) -> None: class RequestAliasRedirect(RoutingException): # noqa: B903 """This rule is an alias and wants to redirect to the canonical URL.""" - def __init__(self, matched_values: t.Mapping[str, t.Any], endpoint: str) -> None: + def __init__(self, matched_values: t.Mapping[str, t.Any], endpoint: t.Any) -> None: super().__init__() self.matched_values = matched_values self.endpoint = endpoint @@ -72,7 +72,7 @@ class BuildError(RoutingException, LookupError): def __init__( self, - endpoint: str, + endpoint: t.Any, values: t.Mapping[str, t.Any], method: str | None, adapter: MapAdapter | None = None, @@ -93,7 +93,10 @@ def _score_rule(rule: Rule) -> float: [ 0.98 * difflib.SequenceMatcher( - None, rule.endpoint, self.endpoint + # endpoints can be any type, compare as strings + None, + str(rule.endpoint), + str(self.endpoint), ).ratio(), 0.01 * bool(set(self.values or ()).issubset(rule.arguments)), 0.01 * bool(rule.methods and self.method in rule.methods), diff --git a/src/werkzeug/routing/map.py b/src/werkzeug/routing/map.py index 73671bf94..4d15e8824 100644 --- a/src/werkzeug/routing/map.py +++ b/src/werkzeug/routing/map.py @@ -104,7 +104,7 @@ def __init__( host_matching: bool = False, ) -> None: self._matcher = StateMachineMatcher(merge_slashes) - self._rules_by_endpoint: dict[str, list[Rule]] = {} + self._rules_by_endpoint: dict[t.Any, list[Rule]] = {} self._remap = True self._remap_lock = self.lock_class() @@ -131,7 +131,7 @@ def merge_slashes(self) -> bool: def merge_slashes(self, value: bool) -> None: self._matcher.merge_slashes = value - def is_endpoint_expecting(self, endpoint: str, *arguments: str) -> bool: + def is_endpoint_expecting(self, endpoint: t.Any, *arguments: str) -> bool: """Iterate over all rules and check if the endpoint expects the arguments provided. This is for example useful if you have some URLs that expect a language code and others that do not and @@ -155,7 +155,7 @@ def is_endpoint_expecting(self, endpoint: str, *arguments: str) -> bool: def _rules(self) -> list[Rule]: return [rule for rules in self._rules_by_endpoint.values() for rule in rules] - def iter_rules(self, endpoint: str | None = None) -> t.Iterator[Rule]: + def iter_rules(self, endpoint: t.Any | None = None) -> t.Iterator[Rule]: """Iterate over all rules or the rules of an endpoint. :param endpoint: if provided only the rules for that endpoint @@ -470,14 +470,14 @@ def application(environ, start_response): raise @t.overload - def match( # type: ignore + def match( self, path_info: str | None = None, method: str | None = None, return_rule: t.Literal[False] = False, query_args: t.Mapping[str, t.Any] | str | None = None, websocket: bool | None = None, - ) -> tuple[str, t.Mapping[str, t.Any]]: ... + ) -> tuple[t.Any, t.Mapping[str, t.Any]]: ... @t.overload def match( @@ -496,7 +496,7 @@ def match( return_rule: bool = False, query_args: t.Mapping[str, t.Any] | str | None = None, websocket: bool | None = None, - ) -> tuple[str | Rule, t.Mapping[str, t.Any]]: + ) -> tuple[t.Any | Rule, t.Mapping[str, t.Any]]: """The usage is simple: you just pass the match method the current path info as well as the method (which defaults to `GET`). The following things can then happen: @@ -770,7 +770,7 @@ def make_redirect_url( def make_alias_redirect_url( self, path: str, - endpoint: str, + endpoint: t.Any, values: t.Mapping[str, t.Any], method: str, query_args: t.Mapping[str, t.Any] | str, @@ -786,7 +786,7 @@ def make_alias_redirect_url( def _partial_build( self, - endpoint: str, + endpoint: t.Any, values: t.Mapping[str, t.Any], method: str | None, append_unknown: bool, @@ -827,7 +827,7 @@ def _partial_build( def build( self, - endpoint: str, + endpoint: t.Any, values: t.Mapping[str, t.Any] | None = None, method: str | None = None, force_external: bool = False, diff --git a/src/werkzeug/routing/rules.py b/src/werkzeug/routing/rules.py index 75323357c..6a02f8d3e 100644 --- a/src/werkzeug/routing/rules.py +++ b/src/werkzeug/routing/rules.py @@ -453,7 +453,7 @@ def __init__( subdomain: str | None = None, methods: t.Iterable[str] | None = None, build_only: bool = False, - endpoint: str | None = None, + endpoint: t.Any | None = None, strict_slashes: bool | None = None, merge_slashes: bool | None = None, redirect_to: str | t.Callable[..., str] | None = None, @@ -493,7 +493,7 @@ def __init__( ) self.methods = methods - self.endpoint: str = endpoint # type: ignore + self.endpoint: t.Any = endpoint self.redirect_to = redirect_to if defaults: From 71b69dfb7df3d912e66bab87fbb1f21f83504967 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 2 May 2024 11:55:52 -0700 Subject: [PATCH 5/7] 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. --- CHANGES.rst | 5 ++++ docs/debug.rst | 35 +++++++++++++++++++++++---- src/werkzeug/debug/__init__.py | 10 ++++++++ src/werkzeug/debug/shared/debugger.js | 4 +-- src/werkzeug/serving.py | 3 +++ 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5658080c3..4d0081e93 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 diff --git a/docs/debug.rst b/docs/debug.rst index 25a9f0b2d..d842135a7 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 a55480aa3..cda1fa2e7 100644 --- a/src/werkzeug/debug/__init__.py +++ b/src/werkzeug/debug/__init__.py @@ -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"): @@ -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") diff --git a/src/werkzeug/debug/shared/debugger.js b/src/werkzeug/debug/shared/debugger.js index f463e9c77..18c658344 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 7f6ea922e..859f9aacb 100644 --- a/src/werkzeug/serving.py +++ b/src/werkzeug/serving.py @@ -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 From 890b6b62634fa61224222aee31081c61b054ff01 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 3 May 2024 14:49:43 -0700 Subject: [PATCH 6/7] only require trusted host for evalex --- src/werkzeug/debug/__init__.py | 25 ++++++++++++++++++++----- src/werkzeug/sansio/utils.py | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/werkzeug/debug/__init__.py b/src/werkzeug/debug/__init__.py index cda1fa2e7..6bef30fbc 100644 --- a/src/werkzeug/debug/__init__.py +++ b/src/werkzeug/debug/__init__.py @@ -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 @@ -352,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, ) @@ -380,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: @@ -390,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 = {} @@ -442,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) @@ -497,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:" @@ -514,8 +531,6 @@ 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") @@ -527,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 diff --git a/src/werkzeug/sansio/utils.py b/src/werkzeug/sansio/utils.py index 48ec1bfa0..14fa0ac88 100644 --- a/src/werkzeug/sansio/utils.py +++ b/src/werkzeug/sansio/utils.py @@ -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. From f9995e967979eb694d6b31536cc65314fd7e9c8c Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 5 May 2024 16:02:12 -0700 Subject: [PATCH 7/7] release version 3.0.3 --- CHANGES.rst | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4d0081e93..f6158e79b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,7 @@ Version 3.0.3 ------------- -Unreleased +Released 2024-05-05 - Only allow ``localhost``, ``.localhost``, ``127.0.0.1``, or the specified hostname when running the dev server, to make debugger requests. Additional diff --git a/pyproject.toml b/pyproject.toml index bde199657..eb06882df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "Werkzeug" -version = "3.0.3.dev" +version = "3.0.3" description = "The comprehensive WSGI web application library." readme = "README.md" license = {file = "LICENSE.txt"}