Vulnerability

Password Reset Poisoning
detect, understand, remediate

Password reset poisoning lets an attacker rewrite the domain in a victim's reset email by manipulating the Host or X-Forwarded-Host header. The reset token is generated against a real account but delivered as a link to an attacker-controlled domain, leading to silent account takeover without any credential phishing.

No credit card required. Free plan available forever.

Severity

High

CWE ID

CWE-640

OWASP Top 10

A07:2021 - Identification and Authentication Failures

CVSS 3.1 Score

7.4

What is password reset poisoning?

Password reset poisoning is an account takeover technique that abuses applications which build the password reset URL from a request-controlled value, most commonly the HTTP Host header. The attacker triggers a real reset for a victim's email address, but supplies a Host header pointing to a domain they control. The application generates a valid, single-use reset token against the victim's account and emails a link of the form https://attacker.com/reset?token=.... When the victim clicks the link out of habit (or is socially engineered into clicking by a follow-up message), the attacker captures the token and resets the password.

The vulnerability is tracked under CWE-640 (weak password recovery mechanism for forgotten password) and is the most common impactful chain on top of host header injection (CWE-644). It is rarely a single-bug finding: the reset flow is broken because the host header is trusted, and the host header is trusted because the framework default was never overridden. Treating it as one or the other in a client report tends to leave half the chain unfixed.

Severity is application-specific. On a consumer app where users routinely receive reset emails, the social pressure to click the link is high and the realistic impact approaches account takeover at scale. On an admin console where resets are rare and inspected, the same bug is harder to weaponise but still produces a high-confidence finding because the proof is the email content itself. Calibration matters more than a default CVSS lookup, which the severity calibration research covers in depth.

How it works

1

Identify the reset endpoint

The attacker finds the password reset request endpoint and confirms it accepts an email address belonging to a victim account, returning a generic acknowledgement to avoid email enumeration.

2

Manipulate the Host header

The attacker submits the reset request with a Host header set to a domain they control, or supplies an override header such as X-Forwarded-Host, X-Host, or X-Forwarded-Server that the application trusts upstream of validation.

3

Application emails poisoned link

The reset email is sent to the victim's real inbox containing a link whose token is valid for the victim's account but whose domain points at the attacker. The token is signed against the user; the URL is not.

4

Attacker captures the token

When the victim clicks the link, the request lands on the attacker's server with the token in the query string or fragment. The attacker replays the token against the legitimate site and resets the password before expiration.

Variants seen in the wild

The classic Host-header swap is only the most obvious form of password reset poisoning. Real engagements turn up several variants, each requiring its own remediation step. A fix that addresses only the headline case usually leaves at least one variant working.

VariantHow the link is poisonedWhy it survives a partial fix
Direct Host header swapRequest sent with Host: attacker.com to the application origin.A whitelist of valid Host values catches this, but only if it runs before the URL is constructed.
X-Forwarded-Host overrideRequest includes X-Forwarded-Host: attacker.com that the framework prefers when behind a proxy.Fixes that whitelist Host but still trust X-Forwarded-Host from external clients leave the bug intact.
Dual host (comma-separated)Request sets Host: legit.com,attacker.com or sends two Host headers; one parser picks the first, another the second.Whitelists that match by prefix or substring miss the trailing attacker domain, especially on parsers that take the last value.
Port injectionHost: legit.com:attacker.com causes URL builders that concatenate host:port to render attacker.com inside the link.Validators that allow the apex domain still pass, because legit.com appears unchanged before the colon.
Path-relative reset linkThe application emits a reset link as a path-relative URL combined with a poisoned host header echoed in another tag.Email clients render the resolved absolute URL using the poisoned host, even though the application thinks it sent a relative path.
Open-redirect chained resetThe link points to legit.com but the next-page or return parameter redirects through an open redirect that leaks the token in Referer.Host validation alone does nothing, because the link is on the legitimate domain. The redirect must also be locked down.

Common causes

Building reset URLs from request.Host at runtime

The application reads the incoming Host header to determine the domain for the reset link rather than using a server-side configured base URL. Every framework offers a request.host helper, and developers often reach for it without realising it is attacker-controlled.

Trusting X-Forwarded-Host from external clients

Frameworks deployed behind a CDN or load balancer enable forwarded-header parsing globally. When the proxy does not strip these headers from external requests, the application reads the attacker-supplied value as if it came from the trusted proxy.

Templated emails using request-derived absolute URLs

The email template includes an {{ reset_url }} placeholder that is rendered using the Host header at request time. The token generation is correct; the link rendering is not.

Tokens signed against the user but not the URL

The reset token is bound to the user account but not to the destination URL, so any URL carrying the token resolves the reset. Binding the token to the canonical URL closes the chain even when the host is misrepresented.

No domain check on the token-redemption endpoint

The endpoint that consumes the reset token accepts any request that presents a valid token, regardless of which domain the user was on when they clicked. A simple Origin or Referer check on the redemption endpoint blocks most poisoning chains in practice.

Reset-link generation in the client portal flow

For SaaS platforms with branded client portals, the reset URL must respect the tenant subdomain. Naive implementations that derive the subdomain from the request rather than from the user record are open to poisoning even when the apex domain is locked down.

How to detect it

Automated detection

  • SecPortal's authenticated scanner drives a real reset request with a manipulated Host header (and the common override headers) and inspects the resulting redirect chain or reflected URL for the injected domain.
  • The external scanner enumerates reset and forgot-password endpoints, fingerprints the framework, and flags configurations where forwarded-host parsing is on without an upstream stripper.
  • Findings carry the request and response pair plus the rendered reset URL, so the chain is reproducible against the asset rather than reconstructed during retest.

Manual testing

  • Submit a password reset request for a controlled test account with the Host header rewritten to a domain you own. Check the inbox for the rendered reset link and confirm whether the host portion was rewritten.
  • Repeat with X-Forwarded-Host, X-Host, X-Forwarded-Server, and X-HTTP-Host-Override. Some frameworks honour multiple variants, and a fix that catches one rarely catches all.
  • Try dual-host payloads (Host: legit.com,attacker.com) and port-injection payloads (Host: legit.com:attacker.com) to surface parser edge cases that a simple allowlist may not cover.
  • For multi-tenant SaaS, repeat each test against the tenant subdomain and against a tenant-on-tenant scenario where the requesting tenant is different from the target user's tenant.

How to fix it

Generate reset URLs from server-side configuration

Store the canonical base URL (or the per-tenant base URL, looked up from the user record) in server-side configuration and use only that value when constructing the reset link. Never derive the host from the incoming request.

Reject or strip forwarded-host headers from external sources

If the application sits behind a trusted proxy, configure it to overwrite X-Forwarded-Host with the validated value rather than appending. Configure the application to ignore forwarded-host headers entirely unless the request originates from a known proxy IP.

Allow-list the Host header at the edge

Configure the web server, load balancer, or CDN to reject requests whose Host header does not match a configured set of valid hostnames. Reject with a 4xx response rather than silently rewriting; silent rewrites mask the underlying misconfiguration.

Bind the reset token to the canonical URL

Sign or hash the reset token against both the user identifier and the canonical reset URL. The token-redemption endpoint then verifies that the URL the user clicked matches the URL the system would have generated, breaking the chain even if the host header was manipulated.

Add a same-origin check on token redemption

When the reset token is consumed, require an Origin or Referer header that matches the application's configured base URL. Cross-origin redemptions, including those from attacker-controlled domains, are rejected before the password is changed.

Short token lifetime and one-time use

Limit reset tokens to single use and a short lifetime (15 minutes is a common upper bound). Combined with the rest of the controls, this turns a successful poisoning into a race the attacker is likely to lose, and a failed reset into a high-signal alert.

Alert on reset link click anomalies

Log the User-Agent, IP, and Referer for each reset-token redemption and alert when a token is presented from an unexpected geography or browser fingerprint relative to the requesting account. This is the highest-signal detection for attacks that slipped past the input controls.

Reporting and triage in the engagement

Password reset poisoning is one of the few findings where the proof of impact is a screenshot of an email rather than a curl request. The credible report includes the manipulated request, the email content received at the test inbox, the rendered URL, and the redemption test that confirms the token works against the legitimate domain. SecPortal's findings management attaches the request, response, and email evidence to the same finding so the chain stays attached during retest rather than being reconstructed.

Severity must be calibrated against business impact. A reset poisoning on a consumer email login is high or critical, because users are conditioned to click reset links. The same bug on a dormant admin tool is medium, because the realistic delivery vector requires a follow-up phishing email and an unmonitored inbox. The finding triage guide covers how to defend severity calls when the client pushes back.

Retest scope must include every variant tested in the original engagement, not just the one the developer fixed. Remediation tracking keeps the original variant matrix attached to the retest, and the retest workflow guide covers how to script the variant suite so it can be replayed exactly during verification.

Compliance impact

Detect password reset poisoning before attackers do

SecPortal exercises password reset flows with manipulated Host and X-Forwarded-Host headers, captures the outgoing email link on the finding, and ships the chain through your branded client report. Start free.

No credit card required. Free plan available forever.