Trusted hosts
Host is a request header, so it is client-controlled. Anything derived from it is
client-controlled too — most importantly url_for(..., _external=True), which is what
builds password reset links and confirmation URLs.
The classic consequence is a reset email whose link points at a host the attacker chose, carrying a token that is valid for your application.
Flask 3.1 added TRUSTED_HOSTS to check this. It defaults to None, which means no
check.
Observed
Section titled “Observed”With TRUSTED_HOSTS = ["example.com"]:
Host: example.com -> 200Host: evil.test -> 400The check happens early, before the view runs, so a rejected request never reaches your code.
TRUSTED_HOSTSHost values Flask will serve; None means no validation at all. The trap is the version boundary rather than the value — this key is new in Flask 3.1, and on 3.0 or earlier Flask silently ignores unknown config keys, so the exact same line reads like a working control and does nothing.
- accepts
["example.com"]|[".example.com"] (subdomain wildcard)|None- default
None- set in
app.config
app.config["TRUSTED_HOSTS"] = ["example.com", "www.example.com"]Setting SERVER_NAME is the older habit and it is not the same control. SERVER_NAME
tells Flask what hostname to use when generating external URLs outside a request
context, and it also constrains routing in ways that surprise people — it is a URL
generation setting that happens to have host-shaped side effects. TRUSTED_HOSTS is
the validation.
If you generate URLs from background jobs you will want both, and they are answering different questions.
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: evil.test' https://app.example.com/400 is correct. 200 means the header is not being validated.
Check the config, since a None here is easy to miss:
python3 -c "from app import app; print('TRUSTED_HOSTS =', app.config.get('TRUSTED_HOSTS'))"And find what is generating external URLs, since that is where a forged host does its damage:
grep -rn '_external=True' --include='*.py' --include='*.html' . | head -20Each of those builds an absolute URL from the request’s host unless SERVER_NAME is
set.
On Flask 3.0 and earlier
Section titled “On Flask 3.0 and earlier”TRUSTED_HOSTS does not exist before 3.1. Setting it on an older version is silently
ignored — Flask does not reject unknown config keys, so the line looks effective and does
nothing.
That is worth checking before you rely on this page:
python3 -c "import flask; print(flask.__version__)"On an older version the options are to upgrade, to validate in a before_request hook,
or to have the proxy reject unknown hostnames — which many deployments already do.
The proxy may already cover this
Section titled “The proxy may already cover this”If a reverse proxy in front of you refuses unknown hostnames — a default server block
that closes the connection, or a load balancer routing strictly by host rule — then
forged Host headers never arrive, and this control is a second layer.
Worth establishing which situation you are in, because it changes this from an open hole to defence in depth. It also changes where the failure appears when the list is wrong: a proxy-level rejection looks nothing like Flask’s 400.
Two layers is still right. The proxy covers what clients reach; TRUSTED_HOSTS covers
anything that reaches the application port directly.
An incomplete list returns 400 to real users, and Flask’s 400 page is unbranded enough
to read as a total outage.
The hostnames that get missed are the ones nobody thinks of as hostnames: the health
check probing by container IP, the internal service DNS name other services use,
localhost from a sidecar, and the platform’s own generated hostname.
A health check failing this way is the worst version — the probe fails, the orchestrator restarts the container, and it presents as a crash loop rather than a config problem.
Enumerate from live traffic first:
grep -ohiE '^host: .*' access.log | sort | uniq -c | sort -rn | head -20Include every hostname that appears with meaningful volume, plus the probe’s, before turning it on.
Related
Section titled “Related”- The production server — the other setting that depends on what is in front of you
- Session cookies — the other place a forged host changes behaviour