A password-reset endpoint often needs to send an absolute URL such as https://accounts.example/reset?.... A tempting implementation is to take the hostname from the incoming HTTP request and prepend it to the reset path.
That shortcut creates a trust problem. The request’s authority information, commonly exposed to application code through Host, :authority, or proxy-derived host fields, describes where the client says the request is addressed. It is not proof that the value is an approved public origin for links containing sensitive tokens.
If attacker-controlled request data can choose the origin of a password-reset, email-verification, invitation, or similar link, a valid token may be sent inside a URL pointing somewhere the application did not intend. The safer design is to build security-sensitive URLs from trusted configuration, then use request host information only where the application genuinely needs it and has validated it.
This article explains that trust boundary, shows the smallest useful design, and covers the proxy and multi-domain cases that make the decision less obvious.
Separate request routing from trusted URL generation
HTTP needs authority information so servers and intermediaries can route requests to the intended origin. In HTTP/1.1 this is commonly represented by the Host header. HTTP/2 and HTTP/3 use the :authority pseudo-header in relevant cases.
That information has a routing role. An application can still receive an unexpected authority value because routing configuration, reverse proxies, development environments, or deliberate requests may present values the application did not expect.
The key mental model is:
request authority: where this request says it is addressed
trusted origin: where the application is allowed to send usersSometimes those strings happen to be identical. They still come from different trust decisions.
For a security-sensitive link, the origin should be an application-controlled value such as:
https://accounts.exampleThe application can then append a known path and an encoded token value:
trusted origin + reset path + reset tokenThe request does not get to choose the destination that will later receive the token.
See the failure as a data-flow problem
Consider a simplified password-reset flow. The application receives a reset request, creates a one-time token, builds an absolute URL, and emails that URL to the account owner.
A risky design looks like this:
incoming request
|
+--> request host -------------------+
| |
+--> account --> reset token |
| |
+----> build absolute URL
|
v
emailThe problem is not that absolute URLs are inherently dangerous. The problem is that untrusted request metadata controls the origin of a URL carrying a security credential.
If the application accepts an unexpected host and uses it to construct the email, the resulting link can have the right path and a genuine token while pointing at the wrong origin. A user who follows that link can disclose the token to that destination. If the token is sufficient to complete account recovery, the consequence can be unauthorized account access.
The defensive change is small but important:
trusted configuration --> approved origin --+
|
account --> reset token ---------------------+--> build URL --> emailNow the attacker can still submit a password-reset request if the endpoint is public, but that request cannot choose where the reset link points.
Build sensitive links from explicit configuration
For an application with one public account origin, the simplest useful design is usually the strongest one: configure that origin explicitly.
For example:
ACCOUNT_ORIGIN = https://accounts.exampleAt startup, parse and validate the value as a URL. Reject configuration that does not meet the application’s rules, such as an unexpected scheme, missing host, embedded credentials, or an origin outside the deployment’s approved set.
When generating a reset link, use the parsed configured origin rather than reconstructing an origin from the current request:
origin = configured_account_origin
path = "/reset-password"
token = newly_generated_reset_token
link = build_url(origin, path, {"token": token})This is pseudocode, not a recommendation to concatenate strings manually. Production code should use the platform’s URL-building facilities so path and query components are encoded according to their roles.
The security property comes from the source of origin: deployment configuration under administrative control, not client-controlled request metadata.
This also makes the behavior easier to test. A request with an unexpected host should either be rejected at an earlier boundary or produce the same approved link origin. It should never silently change the destination of the security-sensitive link.
Validate incoming hosts as a separate control
Using a configured origin for generated links does not mean host validation is unnecessary.
Applications and front-end servers often need to decide which hostnames they will serve. Rejecting unknown hosts reduces ambiguity in routing and can limit other classes of host-dependent behavior. That check belongs at the earliest practical trusted boundary, such as the edge proxy, web server, framework host-validation feature, or application entry point.
A simple policy might be:
accepted request hosts:
- app.example
- accounts.exampleRequests for other hosts are rejected rather than falling through to a default virtual host.
This control and trusted URL generation solve related but different problems:
- Host validation limits which authorities the application will accept for incoming requests.
- Trusted-origin URL generation limits which origins the application will place into sensitive outbound links.
Using both is useful defense in depth. If a routing mistake causes an unexpected host to reach the application, configured URL generation still keeps reset links on the intended origin. If a developer later adds host-dependent behavior elsewhere, early host validation reduces the set of values that behavior can receive.
Be precise about reverse proxies
Reverse proxies make host handling easy to misunderstand. An application behind a proxy may see the proxy’s connection details while the framework reconstructs an external URL from forwarding metadata such as the standardized Forwarded header or deployment-specific X-Forwarded-* fields.
Those fields are trustworthy only under a specific deployment assumption: an identified trusted proxy removes or overwrites client-supplied forwarding values and supplies the values the application is meant to consume.
If an application blindly trusts forwarding headers from any client, moving from Host to X-Forwarded-Host does not fix the trust problem. It merely changes which request field controls the result.
Configure proxy trust narrowly. The exact mechanism is framework- and infrastructure-specific, but the invariant is portable:
internet client
|
v
trusted edge proxy
- validates public routing
- replaces trusted forwarding metadata
|
v
application
- trusts forwarding metadata only from that boundaryEven with correctly configured proxy trust, a security-sensitive email link usually does not need to derive its origin from forwarding metadata when the application’s public origin is already known. Prefer the simpler configured value unless the product genuinely supports multiple approved origins.
Handle multiple legitimate origins deliberately
Some applications serve several customer domains, regions, or branded front ends. A single hard-coded origin may not fit that design.
The wrong response is to make any request host valid. Instead, make the origin a result of trusted application state.
Suppose each tenant has a verified public domain. The flow can be:
account -> tenant_id -> verified tenant origin -> build reset URLThe selected origin comes from a trusted record associated with the account, not from whatever host appeared on the reset request.
If request context is legitimately part of origin selection, constrain it to an approved set and bind it to the relevant account or tenant. For example, a request to customer-a.example should not cause a reset link for a customer-b account to use customer A’s origin merely because both domains are valid globally.
This is an authorization-like question: not only “is this host allowed?” but also “is this origin allowed for this account and this operation?”
Multi-domain systems also need a recovery plan for domain changes. Removing a domain from service should update the trusted mapping before new security links are issued. Existing tokens need behavior consistent with their lifetime and validation rules; changing the link origin should not silently extend or revive them.
Keep the token’s own defenses intact
A trusted origin protects where a sensitive link points. It does not make a weak reset token strong or repair an unsafe recovery flow.
Password-reset and similar tokens still need appropriate properties for their purpose. They should be generated with enough unpredictability for the threat model, scoped to the intended action and account, protected in storage as appropriate to the design, expire within a justified period, and become unusable after successful consumption when the protocol requires one-time use.
Transport protection matters too. A link carrying a bearer-style recovery token should normally use HTTPS so network observers do not receive the token in plaintext during transit.
The application should also avoid leaking these URLs into places that do not need them. Request logs, analytics systems, third-party resources, browser history, and support tooling can all become additional exposure paths depending on how the flow is implemented.
These controls address different failure modes. A perfectly generated token sent to an attacker-chosen origin is still exposed. A correctly configured origin does not compensate for a predictable or indefinitely reusable token.
Avoid partial fixes that preserve the same trust mistake
Several fixes look plausible while leaving the core data flow unchanged.
Checking that the host “looks like a domain” verifies syntax, not authorization. An attacker-controlled domain can be syntactically valid.
Checking whether a hostname contains an approved string is also fragile. Security decisions should compare parsed hostnames against exact approved values or use carefully defined subdomain rules when subdomains are genuinely trusted.
Replacing Host with a forwarding header is not a defense unless the forwarding chain itself is trusted and correctly configured. Even then, a configured origin is simpler when request-specific origin discovery is unnecessary.
Generating a relative URL can be appropriate for links rendered inside a page because the browser already has an origin. It does not solve an email’s need for an absolute destination; something still has to choose the origin before the recipient can navigate to it.
Finally, validating the host only inside the password-reset handler leaves other host-dependent features exposed to inconsistent behavior. Central host validation is easier to reason about, while sensitive URL builders should independently use trusted origin data.
Test the trust boundary, not only the happy path
A useful test suite proves that request metadata cannot steer a sensitive link.
For a single-origin application, send otherwise valid reset requests with the normal host, an unexpected host, and forwarding headers that would name another host if trusted. Depending on the architecture, unexpected requests may be rejected before the handler. If they reach the handler, the generated email should still use the configured approved origin.
Also test deployment behavior through the real proxy path. A unit test can prove which variable a URL builder reads, but it cannot prove that a reverse proxy strips spoofed forwarding headers or that the framework’s proxy-trust setting matches the network topology.
For multi-origin applications, test cross-tenant combinations explicitly. A valid domain for one tenant must not become a valid recovery destination for another tenant unless the product intentionally permits that relationship.
Operational monitoring can help detect configuration mistakes. Rejected unknown hosts and failures to map an account to an approved origin can be useful security events, provided logs do not include reset tokens or other unnecessary secrets.
Know what this control does not cover
Trusted-origin URL generation reduces the risk that client-controlled host data redirects security-sensitive links to an unintended origin. Host validation further reduces unexpected authority handling at the request boundary.
Neither control protects against compromise of the trusted configuration store, a malicious administrator who can change approved origins, domain or DNS compromise affecting an approved origin, token leakage through another channel, phishing with unrelated domains, or flaws in the token validation logic itself.
The trust boundary therefore includes more than the URL builder. The configured origin, tenant-domain records, deployment configuration, proxy rules, DNS, TLS termination, and recovery-token verification all need protection appropriate to the application’s risk.
For a low-sensitivity feature that generates ordinary navigation links, deriving an origin from validated request context may be reasonable. For links that carry account-recovery or authentication capability, explicit trusted origin selection is worth the small amount of configuration because the consequence of choosing the wrong destination is much higher.
Make origin selection an explicit security decision
When code needs an absolute security-sensitive URL, do not ask the current request to decide where that URL should lead unless the product truly requires request-specific origin selection.
Start with a configured, parsed, approved origin. Validate incoming hosts separately. If multiple origins are legitimate, select among them using trusted account or tenant state and enforce the relationship explicitly. Then test the full proxy path to confirm that untrusted forwarding metadata cannot change the decision.
That design keeps a simple boundary visible in the code: clients may request a security operation, but they do not get to choose where the resulting credential is sent.