Skip to content

Commit 7b21d43

Browse files
authoredNov 13, 2024··
configure and check request.trusted_hosts (#5637)
2 parents 10bdf61 + 4f7156f commit 7b21d43

File tree

4 files changed

+40
-0
lines changed

4 files changed

+40
-0
lines changed
 

‎CHANGES.rst

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ Unreleased
2626
- Fix how setting ``host_matching=True`` or ``subdomain_matching=False``
2727
interacts with ``SERVER_NAME``. Setting ``SERVER_NAME`` no longer restricts
2828
requests to only that domain. :issue:`5553`
29+
- ``Request.trusted_hosts`` is checked during routing, and can be set through
30+
the ``TRUSTED_HOSTS`` config. :issue:`5636`
2931

3032

3133
Version 3.0.3

‎docs/config.rst

+15
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,21 @@ The following configuration values are used internally by Flask:
258258

259259
Default: ``None``
260260

261+
.. py:data:: TRUSTED_HOSTS
262+
263+
Validate :attr:`.Request.host` and other attributes that use it against
264+
these trusted values. Raise a :exc:`~werkzeug.exceptions.SecurityError` if
265+
the host is invalid, which results in a 400 error. If it is ``None``, all
266+
hosts are valid. Each value is either an exact match, or can start with
267+
a dot ``.`` to match any subdomain.
268+
269+
Validation is done during routing against this value. ``before_request`` and
270+
``after_request`` callbacks will still be called.
271+
272+
Default: ``None``
273+
274+
.. versionadded:: 3.1
275+
261276
.. py:data:: SERVER_NAME
262277
263278
Inform the application what host and port it is bound to.

‎src/flask/app.py

+7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from werkzeug.routing import Rule
2525
from werkzeug.serving import is_running_from_reloader
2626
from werkzeug.wrappers import Response as BaseResponse
27+
from werkzeug.wsgi import get_host
2728

2829
from . import cli
2930
from . import typing as ft
@@ -183,6 +184,7 @@ class Flask(App):
183184
"SECRET_KEY_FALLBACKS": None,
184185
"PERMANENT_SESSION_LIFETIME": timedelta(days=31),
185186
"USE_X_SENDFILE": False,
187+
"TRUSTED_HOSTS": None,
186188
"SERVER_NAME": None,
187189
"APPLICATION_ROOT": "/",
188190
"SESSION_COOKIE_NAME": "session",
@@ -441,6 +443,11 @@ def create_url_adapter(self, request: Request | None) -> MapAdapter | None:
441443
.. versionadded:: 0.6
442444
"""
443445
if request is not None:
446+
if (trusted_hosts := self.config["TRUSTED_HOSTS"]) is not None:
447+
request.trusted_hosts = trusted_hosts
448+
449+
# Check trusted_hosts here until bind_to_environ does.
450+
request.host = get_host(request.environ, request.trusted_hosts) # pyright: ignore
444451
subdomain = None
445452
server_name = self.config["SERVER_NAME"]
446453

‎tests/test_request.py

+16
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,19 @@ def test_limit_config(app: Flask):
5252
assert r.max_content_length == 90
5353
assert r.max_form_memory_size == 30
5454
assert r.max_form_parts == 4
55+
56+
57+
def test_trusted_hosts_config(app: Flask) -> None:
58+
app.config["TRUSTED_HOSTS"] = ["example.test", ".other.test"]
59+
60+
@app.get("/")
61+
def index() -> str:
62+
return ""
63+
64+
client = app.test_client()
65+
r = client.get(base_url="http://example.test")
66+
assert r.status_code == 200
67+
r = client.get(base_url="http://a.other.test")
68+
assert r.status_code == 200
69+
r = client.get(base_url="http://bad.test")
70+
assert r.status_code == 400

0 commit comments

Comments
 (0)
Please sign in to comment.