An application builds absolute URLs — password-reset links, redirects, canonical tags — from the HTTP Host header of the incoming request. What can go wrong, and how would you make host handling safe at the edge and in the application?
answer
- Host is client input, so is X-Forwarded-Host
- Catch-all vhost = the enabling condition
- Reset link → attacker domain → token theft
- Unkeyed input → cache poisoning
- Fix: default server rejects unknown hosts + configured base URL
basics
~20 sHost is attacker-controlled input. If a catch-all virtual host accepts any Host, an attacker can make the app emit links to their domain — password-reset tokens sent to evil.com — or poison shared caches. Fix: allowlist hostnames at the edge, and build URLs from configured values, not from the request.
solid answer
~60 sThe Host header is supplied by the client, so it is untrusted input, and so are `X-Forwarded-Host` and `Forwarded`. The classic exploit chain: the server has a catch-all virtual host that serves the app for any Host. An attacker triggers a password reset for a victim's address with `Host: attacker.example`. The app renders the reset link from the request host, the victim gets an email pointing at the attacker's domain, clicks, and hands over the token. Variants include poisoning a shared cache so other users receive a page with attacker-controlled absolute URLs or script sources, and reaching an internal vhost through a shared edge that routes on Host without checking authority. Defence is layered: 1. **At the edge**, define an explicit default server that rejects unknown hostnames (`444`/close, or `421`) instead of falling through to the app. Only forward `X-Forwarded-Host` when your own proxy set it. 2. **In the application**, generate absolute URLs from a configured canonical base URL, not from the request. If you must derive it, validate the host against an allowlist. 3. **Cache keys** must include the host, and cached responses must not embed a client-supplied host.
code
http · 5 linesPOST /password/reset HTTP/1.1
Host: attacker.example
Content-Type: application/x-www-form-urlencoded
email=victim%40example.comgo deeper
Say plainly that Host comes from the client and must not be trusted to build links, and that URLs for emails should come from configuration.
Walk the password-reset exploit end to end and name the catch-all virtual host as the enabling condition.
Cover the full layering: default server rejecting unknown hosts, overwriting forwarded headers at the outermost hop, allowlists in the framework, host in the cache key, and how you would test for it.
Set the organisational rule — the request host is a routing input only — decide where host validation lives across edge tiers and multi-tenant routing, and account for cache-key design and trusted-proxy configuration as part of the platform contract.
## The premise: Host is user input A request header is whatever the client typed. `Host: shop.example.com` is not verified by anything — TLS validates the *server's* identity to the client, not the client's claim about which site it wants. The same is true, doubly, of `X-Forwarded-Host`, `X-Forwarded-Server`, and the standard `Forwarded` header: those are hints written by whoever spoke last, and a direct client can write them itself. Frameworks make it easy to forget this. Helpers that produce an absolute URL — `url_for(..., _external=True)`, `UriComponentsBuilder.fromHttpRequest(...)`, `request.build_absolute_uri()`, `$_SERVER['HTTP_HOST']` — read the request host by default. That is convenient and usually correct, until the value is attacker-chosen. ## Exploit 1: poisoned password-reset links The highest-impact case, because it turns a header into account takeover: 1. The origin has a catch-all vhost: any Host reaches the app. 2. The attacker POSTs to `/password/reset` with `[email protected]` and `Host: attacker.example`. 3. The app creates a one-time token and renders the email body with an absolute link derived from the request host: `https://attacker.example/reset?token=…`. 4. The victim receives a genuine email from the genuine sender and clicks. 5. The attacker's server logs the token; the attacker resets the password. Nothing in the flow is a phishing email — the mail is authentic, which is why the click-through rate is high. ## Exploit 2: cache poisoning If a shared cache (CDN, reverse-proxy cache) keys on path but the response reflects the request host into the body — a canonical `<link>`, a script `src` on an assets host, an absolute redirect — an attacker can request the page with `Host: attacker.example` (or, more commonly, `X-Forwarded-Host: attacker.example`, since that is not usually part of the cache key), get it stored, and have subsequent legitimate visitors served a page that loads the attacker's script. The unkeyed-input problem is general: any request field that changes the response but is not in the cache key is a poisoning vector. ## Exploit 3: routing past the edge When an edge terminates TLS and routes to backends by Host, and an internal service is reachable at the same IP, sending an internal hostname in Host can reach services that were never meant to be public. The SNI can be a legitimate public name; the Host does the routing. Related: SSRF payloads that reach an internal address but set Host to a public vhost name. ## Layered defence **At the edge / web server** - Define an explicit **default server** for each listener that does *not* serve the app. In nginx, a `default_server` block returning `444` (close without response) or `421`; in Apache, a first vhost that returns 421/404. This removes the catch-all that all three exploits rely on. - Match hostnames explicitly (`server_name shop.example.com www.shop.example.com;`) rather than with broad wildcards. - **Strip** inbound `X-Forwarded-Host` / `Forwarded` at the outermost hop and set them yourself, so the app can trust them. Never append to a client-supplied value. - Consider validating that the HTTP host matches the TLS SNI on that connection and returning `421 Misdirected Request` when it does not. **In the application** - Build absolute URLs from a **configured canonical base URL** (`app.base-url=https://shop.example.com`), especially for anything that leaves the request/response cycle: emails, webhooks, stored links, signed callbacks. - If a per-tenant host is genuinely needed, resolve it from the *tenant record* keyed by an allowlist lookup, and reject requests whose host is not in the allowlist with 400/421. - Treat trusted-proxy configuration as security config: frameworks that honour forwarded headers (Spring's `ForwardedHeaderFilter`, Rails' `config.hosts`, Django's `ALLOWED_HOSTS`, Express `trust proxy`) must be told which upstreams to believe. Django's `ALLOWED_HOSTS` is the clearest example of the correct pattern: an explicit allowlist enforced before any handler runs. **At the cache** - Include the host in the cache key (most CDNs do by default; verify for multi-tenant setups). - Do not let unkeyed headers such as `X-Forwarded-Host` influence a cacheable response body. ## How to test it Send the request yourself and read the output: `curl -H 'Host: evil.example' https://…` against a staging host, then inspect any generated `Location`, canonical link, or email body. If the attacker's string appears, you have the bug. Repeat with `X-Forwarded-Host` and `Forwarded`, which are the versions people forget after fixing Host. ## The one-line rule Use the request host for **routing**; never for **authority**. Anything durable — an emailed link, a stored URL, a cached body, a redirect target — should come from configuration or an allowlist lookup.
- You fixed the app to use a configured base URL for emails. Is the catch-all virtual host still a problem?Yes. The email vector is closed, but the catch-all still lets anyone route arbitrary hostnames into the app, which keeps cache-poisoning surface open wherever a response reflects the host, allows internal vhosts to be probed through the same listener, and pollutes logs and metrics with attacker-chosen host values. Defence in depth means fixing both the edge and the application.
- When is it legitimate to trust X-Forwarded-Host?Only when your own outermost proxy overwrites it on every inbound request and the application is unreachable except through that proxy. The framework must also be configured with the trusted-proxy set so it knows how many hops to believe. If a client can reach the app directly, or the proxy appends rather than overwrites, the value is attacker-controlled and must be discarded.
Letting the visitor fill in the return address on your own letterhead: the letter is genuine, the signature is yours, and the reply goes to them.
saying these in an interview costs you the question
- Claiming TLS or the certificate validates the Host header — TLS authenticates the server to the client, not the client's host claim.
- Fixing Host but leaving X-Forwarded-Host and Forwarded trusted.
- Treating this as phishing; the email is genuinely from your system, which is what makes it effective.
- Sanitising the host with a blocklist or a regex instead of an allowlist of known hostnames.
- Assuming the framework is safe by default; most absolute-URL helpers read the request host unless configured otherwise.