Applications often need to send absolute URLs. A password reset email, account verification message, or administrative invitation needs a link such as https://accounts.example/reset?..., not just /reset?....
A tempting implementation takes the hostname from the current HTTP request and attaches the security-sensitive path. That works in ordinary testing, but it confuses two different facts: where the request says it was addressed and which public origin the application trusts for security links. If an untrusted request can influence the first value, it may influence a link containing a secret token.
This article explains the defensive rule behind Host header poisoning: treat request authority as routing input, not as trusted configuration. You will see how to choose a trusted origin, how reverse proxies change the trust boundary, and how to verify that sensitive links cannot be redirected by request metadata.
Request authority answers a routing question
In HTTP, the request carries authority information identifying the target host and, optionally, port. HTTP/1.1 commonly carries it in the Host field. HTTP/2 and HTTP/3 use the :authority pseudo-header in the relevant cases.
That information is necessary because one server or proxy can serve many hostnames. For example:
GET /settings HTTP/1.1
Host: accounts.exampleThe server can use accounts.example to select the correct virtual host or application.
The security mistake is to infer more from this value than it proves. Receiving a request whose authority says accounts.example does not, by itself, make that string trusted application configuration. A client supplies request metadata, and a proxy may rewrite it. The application needs an explicit policy for which authorities it accepts and which origin it uses when generating security-sensitive URLs.
A useful mental model is:
request authority -> where should this request be handled?
trusted origin -> where may this application send security links?Those values may normally look identical. They have different trust requirements.
The smallest dangerous pattern
Suppose a password reset handler creates a token and then constructs the email URL from the incoming request:
host = request.authority
reset_url = "https://" + host + "/reset?token=" + token
send_reset_email(user, reset_url)This is simplified pseudocode, but the trust problem is real. The reset token is supposed to give its holder limited authority to reset one account. The application has now placed that token into a URL whose destination depends on request-controlled data.
If the deployment accepts an unexpected authority and passes it to the application, the generated URL can point somewhere other than the intended site. A recipient who follows that link can send the token to the wrong origin. The weakness is therefore not merely a strange hostname appearing in an email. It is a broken binding between a sensitive token and the destination that is supposed to receive it.
The same reasoning applies to other links that carry authority: email verification, account recovery, invitation acceptance, or a one-time administrative action. The consequence depends on what the token can do.
This threat model assumes that an untrusted party can influence authority metadata that reaches link generation. It does not require that the attacker control DNS for the legitimate application domain. It also does not mean every use of Host is vulnerable; routing necessarily uses authority information. The dangerous step is treating unvalidated request authority as the trusted base for a security-sensitive absolute URL.
Generate security links from a trusted origin
For an application with one public security origin, the simplest design is usually to configure that origin explicitly:
SECURITY_ORIGIN = "https://accounts.example"Then link generation uses the configured value:
reset_url = SECURITY_ORIGIN + "/reset?token=" + tokenThe request can still carry a host for routing, but it no longer chooses where the reset token is sent. This changes the causal chain: attacker-controlled request metadata may affect whether a request is accepted, but it cannot change the destination embedded in the email.
Keep the configured origin as a structured value rather than a string assembled from several unrelated settings. Scheme, hostname, and non-default port together define the origin that the browser will contact. If the public service is HTTPS, generate HTTPS links directly instead of guessing the scheme from whatever reached the application server.
A relative URL is also useful when the URL never leaves an already trusted browser context. For example, an internal navigation link can often be /settings/security. Email is different because there is no existing page origin against which the recipient can resolve a relative reference. The sender has to choose an absolute destination, so it needs a trusted source for that destination.
Multi-tenant applications need an explicit mapping
A single hard-coded origin is not enough for every system. A hosted service might legitimately serve:
team-a.service.example
team-b.service.example
customer.example.orgThe safe alternative is not to fall back to whatever hostname appears in the request. Instead, derive the public origin from trusted tenant configuration.
Conceptually:
tenant = load_tenant_from_authenticated_context_or_record()
origin = tenant.verified_public_origin
reset_url = origin + "/reset?token=" + tokenThe hard part moves to enrollment: how does verified_public_origin become trusted? For a platform-owned subdomain, the application can derive it from a tenant identifier under a domain the platform controls. For a customer-owned custom domain, the platform needs a domain-ownership verification process appropriate to its architecture before treating that domain as an approved origin.
The mapping should also define what happens when a tenant has no valid public origin. Failing closed is clearer than silently substituting request authority. A security email can be delayed or use a known platform origin rather than sending a token to an unverified destination.
This is an example of a broader rule: dynamic configuration can still be trusted, but its trust must come from a controlled provisioning process rather than from the current request.
Reject authorities the service does not serve
Trusted link generation fixes the token-destination problem, but the request path should still reject unexpected authorities. RFC 9110 describes host and port information as application-level routing data and notes that it is a target for cache poisoning and unintended routing when not verified.
At the internet-facing layer, define the hostnames the service is expected to receive. Requests for unrelated authorities should not quietly fall through to a default virtual host that runs the application.
For a simple deployment, the policy might be:
accepted authorities:
- app.example
- accounts.example
anything else:
- reject before application routingThis reduces more than link-generation risk. Host validation can also prevent an unexpected authority from selecting the wrong virtual host, influencing redirects, or entering a cache key in a way the deployment did not intend.
Do not implement this as a loose suffix check such as “ends with example”. Hostname comparisons need exact, normalized rules that match the deployment’s actual domain model. If subdomains are intentionally dynamic, validate them according to that model and resolve them to a known tenant rather than accepting arbitrary strings.
Reverse proxies move the trust boundary
Many applications never receive the client’s HTTP connection directly. A load balancer, CDN, ingress proxy, or API gateway terminates the connection and forwards a new request to the application.
That architecture creates two separate questions:
client -> trusted proxy -> application
| |
validates public trusts only the
authority proxy's contract?A proxy may preserve the original authority, replace it, or communicate external scheme and host through forwarding metadata. The exact header names and framework settings vary, so there is no portable instruction to “trust forwarded headers” globally.
The portable rule is narrower: only trust authority metadata from a proxy when the application can establish that the request came through that trusted proxy and the proxy is configured to overwrite client-supplied values according to a documented contract.
If an application accepts the same forwarding headers from direct internet clients, the headers are still attacker-controlled. If multiple proxies are chained, each hop needs a clear ownership rule so the application knows which value represents the validated external request.
Even with a well-configured proxy, security-link generation is usually simpler when it uses configured application or tenant origins. Proxy-derived authority is most useful for routing and ordinary URL generation where the deployment intentionally needs it. Sensitive tokens deserve the smaller trust surface.
HTTP/2 and HTTP/3 do not remove the problem
Calling this issue “Host header poisoning” can make it sound specific to an HTTP/1.1 header. The underlying problem is authority trust.
HTTP/2 uses :authority to carry the authority portion of the target URI, and the protocol specifies how intermediaries translate that value when constructing an HTTP/1.1 Host field. HTTP/3 likewise defines authority handling. A modern proxy can therefore receive one protocol from the client and send another to the backend.
Application code should not try to independently reconcile raw Host, :authority, and proxy-specific forwarding fields. Let the HTTP server and trusted proxy implement protocol semantics, then apply one deployment-level policy to the resulting authority. For sensitive absolute URLs, use the configured trusted origin rather than whichever protocol field happened to carry the incoming value.
This separation avoids a fragile security design where changing the frontend protocol changes which hostname the application trusts.
Common fixes that leave the trust problem intact
One weak fix is to sanitize the host only enough to make it look like a hostname. A syntactically valid hostname can still be the wrong destination. Validation needs to answer “is this an authority this service trusts?”, not merely “can this string be parsed?”
Another mistake is to validate the hostname when the request arrives but later generate links from a different forwarding field. The security invariant must cover the exact value consumed by link generation.
A third mistake is to maintain an allowlist but accept an unknown host by falling back to the first configured site. That may keep the application running, yet it hides routing errors and can make behavior depend on server ordering. Rejecting unexpected authorities gives operators a visible failure to investigate.
Finally, don’t assume a strong reset token compensates for an untrusted destination. Token entropy protects against guessing. It does not help if the application itself places the token in a URL directed to the wrong origin.
Verify the control at the production boundary
A useful test starts from the outside of the real request path, not from a unit test that bypasses the proxy.
Send a harmless request with an authority that the service does not recognize. The edge should reject it, or the request should otherwise fail according to the documented routing policy. Then exercise security-link generation with test accounts while varying every authority-related input the production stack accepts. The generated destination should remain the configured application or tenant origin.
Also test normal deployment cases: the canonical hostname, any deliberate aliases, non-default ports if they are supported, and tenant custom domains after verification. A control that rejects legitimate traffic will eventually be bypassed operationally, so expected authorities should be explicit and testable.
Logging rejected authorities can help detect configuration mistakes and suspicious traffic, but avoid logging secret URL query values or reset tokens. Host validation is a preventive routing control; logs are complementary evidence, not a substitute for it.
Know what this control does not solve
Trusted-origin link generation reduces the risk that request authority redirects a security token to an unintended origin. Host allowlisting also reduces several forms of misrouting. Neither control proves that the destination application itself is uncompromised, that the recipient’s mailbox is trustworthy, or that a reset token cannot leak through another channel.
Password recovery still needs appropriately random, short-lived, single-use tokens and careful handling after redemption. Sensitive pages may need controls against token leakage to third parties. Reverse proxies still need correct TLS, routing, and forwarding configuration. Those are separate layers with different failure modes.
There is also an operational trade-off. A fixed origin is easy to reason about but unsuitable when customers legitimately use verified custom domains. A tenant mapping adds provisioning and recovery complexity, but it keeps the set of trusted destinations explicit. Choose the simpler model when the product permits it; add dynamic origins only when there is a real requirement.
Make the origin a security decision
When code needs an absolute security link, ask where its authority comes from. If the answer is “the current request,” the design is coupling a security decision to data supplied for routing.
Use a configured origin for single-origin applications and a verified configuration mapping for legitimate multi-tenant origins. Reject authorities the service does not intend to serve, and define exactly which proxy may supply external request metadata. Then test the whole path from the public edge to link generation.
The practical invariant is small enough to remember: requests can tell the server where they claim to be going; they should not get to choose where your security secrets are sent.