Password Reset Links Need a Trusted Public Origin

A password reset email often contains one of the most sensitive URLs an application creates. Possession of a valid reset token may be enough to establish a new credential for the associated account, so the destination embedded in that URL is part of the security boundary.

A common implementation mistake is to construct the absolute reset URL from host information carried by the incoming HTTP request. Headers such as Host exist for request routing, and deployments behind proxies may also expose forwarded host or scheme metadata. Unless the application has explicitly established which intermediary is trusted and which values are valid, that request metadata is not a safe source of authority for a security-sensitive outbound link.

The safer design is simpler: configure the application’s public origin from trusted deployment state and append the reset path and token to that origin.

Request routing metadata is not application identity

An HTTP request needs enough information for infrastructure to route it. That does not mean every routing value is a trusted statement about the application’s canonical external identity.

Consider a reset handler that effectively performs this operation:

origin = "https://" + request.host
reset_url = origin + "/reset?token=" + token
send_email(account.email, reset_url)

If an untrusted client can influence request.host, the application can create a valid token and place it in a URL whose authority points somewhere else. The cryptographic quality of the token does not correct the destination error. A high-entropy token sent to the wrong origin is still exposed to that origin when the recipient follows the link.

The same design concern applies when frameworks reconstruct an external URL from X-Forwarded-Host, Forwarded, or similar proxy metadata. Such headers can be legitimate inside a controlled proxy chain, but they require an explicit trust model. Accepting them directly from arbitrary clients turns deployment metadata into attacker input.

Reset URL construction should start from configuration

A deployment normally has a small set of public origins that are valid for account recovery. Store those origins in configuration controlled by the operator rather than deriving them from each reset request.

For a single-origin application, the rule can be direct:

PUBLIC_ORIGIN = "https://accounts.example.com"

token = create_reset_token(account)
reset_url = PUBLIC_ORIGIN + "/reset?token=" + url_encode(token)
send_email(account.email, reset_url)

The request can still carry host metadata for routing, logging, or virtual-host selection. It simply does not decide where a credential-recovery token is sent.

Multi-tenant systems need a more deliberate mapping. If tenants legitimately use different domains, resolve the public origin from trusted tenant configuration after the tenant identity has been established. Do not let a caller provide an arbitrary origin merely because the application supports custom domains.

Rejecting unexpected hosts at the edge or application layer is useful. It reduces ambiguous virtual-host behavior and can block requests that should never reach the application. It should not be the only control protecting reset URL generation.

A configured public origin gives the URL builder an independent source of truth. Even if a routing layer later changes, or a new proxy header becomes visible to the application, the reset link does not silently inherit that value.

Conversely, a trusted public origin does not remove the need for host validation. Unexpected host values can affect redirects, cache keys, absolute links, virtual-host routing, and other behavior. The two controls protect different points in the request and response path.

Proxy trust must be explicit

Reverse proxies commonly terminate TLS and forward requests to an application over an internal connection. Frameworks may use forwarded headers to reconstruct the external scheme and host. That can be correct when the application accepts those headers only from a trusted proxy and the proxy replaces client-supplied values with authoritative ones.

The dangerous configuration is a mixed trust boundary: the application treats forwarded metadata as authoritative while an external client can supply or preserve the same metadata.

A robust deployment defines which proxy hops are trusted, strips or overwrites relevant incoming forwarding headers at the boundary, and configures the application framework to honor proxy metadata only under those conditions. Security-sensitive link generation should still prefer explicit application configuration when the expected public origin is known in advance.

The reset token needs its own lifecycle controls

Fixing the origin protects the delivery destination, but it does not make the reset token safe by itself. The token should be generated with sufficient unpredictability, bound to the intended account or recovery transaction, expire after a bounded period, and become unusable after successful consumption.

Applications should also avoid exposing the token through unrelated page behavior. A reset page that loads third-party resources can create additional disclosure paths depending on browser referrer behavior and page design. Keeping recovery pages narrow and setting an appropriate Referrer-Policy reduces unnecessary propagation of sensitive URL data.

After a token is accepted, the server still has to enforce the recovery workflow: verify the token, apply the intended password policy, invalidate the token after use, and handle existing sessions according to the application’s account-security policy.

Testing should exercise the trust boundary

A useful test changes host-related request metadata during reset initiation and then inspects the generated email. The destination should remain the configured application origin.

Test the values that the actual deployment stack exposes, including Host and any forwarded host or scheme fields that survive the proxy. Also test malformed or unexpected host values to confirm that routing validation behaves as intended.

For multi-tenant deployments, verify that one tenant cannot select another tenant’s recovery origin and that an unregistered custom domain cannot enter the mapping. The expected origin should come from trusted tenant state, not from a URL parameter or unchecked header.

These tests are stronger when they inspect the final artifact delivered to the user rather than only the intermediate request object. The security property is concrete: a valid recovery token must not be embedded in a link to an origin selected by an untrusted requester.

Keep authority separate from input

Password reset flows cross several boundaries at once: an unauthenticated request starts the process, the server creates a privileged token, an email transports it, and a browser later returns it to the application. The origin in the reset URL connects those stages.

Treating that origin as deployment authority rather than request input keeps the chain anchored to a destination the operator selected. Host validation, disciplined proxy trust, token lifecycle controls, and narrow recovery pages then reinforce the same boundary without depending on any single header to define application identity.