The Trusted Email That Wasn't
The email looked perfect.
It came from the right sender address. SPF passed. DKIM signature valid. DMARC alignment confirmed. Every automated check said: this is a legitimate message from the platform. Your email client didn't flag it. Your spam filter didn't catch it. There was nothing to catch.
Except the link inside pointed to a domain the attacker controlled.
The Setup
The target was a financial platform — the kind that holds real money. Users log in, manage accounts, move funds. The standard stuff. And like every platform that handles credentials, it had a password reset flow.
The flow worked like this:
- User enters their email on the "Forgot password" page
- Server generates a unique reset token
- Server sends an email containing a link with that token
- User clicks the link, enters a new password
- Token is consumed, password is changed
Simple. Standard. Implemented a thousand times across the internet.
The problem was in step 3.
The Discovery
When the server generated the password reset email, it needed to build a URL. Something like:
https://platform.example.com/reset?token=abc123def456
To build that URL, the server needed to know its own domain. And instead of using a hardcoded configuration value, it asked the incoming HTTP request.
Specifically, it read the X-Forwarded-Host header.
This header exists for a legitimate reason. When a request passes through a reverse proxy or load balancer, the original Host header gets replaced. X-Forwarded-Host preserves the original value so the backend knows what domain the user actually requested.
But here's the thing: anyone can set this header.
There's no authentication on HTTP headers. There's no signature. A client can send whatever X-Forwarded-Host value they want, and if the server trusts it without validation, it becomes part of every URL the server generates.
The Attack
The request was simple:
http
POST /users/password HTTP/2
Host: platform.example.com
X-Forwarded-Host: attacker.example.com
Content-Type: application/x-www-form-urlencoded
email=victim@example.comThe server processed this normally. It generated a reset token. It built the email. But when it constructed the reset URL, it used the attacker's domain:
https://attacker.example.com/reset?token=abc123def456
Then it sent the email.
Why It Worked
The email was legitimate in every technical sense. It was sent by the platform's actual mail servers. The sender address was real. All email authentication mechanisms confirmed its authenticity.
The victim had no reason to suspect anything. The email said "Click here to reset your password." The link was blue and underlined. They clicked it.
Their browser navigated to the attacker's domain. The attacker's server captured the reset token from the URL. Then the attacker used that token on the real platform to set a new password.
Account compromised. The victim's funds, their personal information, their transaction history — all accessible.
The Chain
What made this particularly dangerous wasn't just the header injection. It was the cascade:
- Header injection — The server trusted a client-supplied header for URL generation
- Email trust — The resulting email was genuinely from the platform, passing all checks
- Token capture — The victim's click sent the reset token to the attacker
- Account takeover — The attacker used the legitimate token on the real platform
Each step relied on the previous one. The header injection alone was concerning. Combined with the email flow, it was devastating.
And it didn't stop at password resets. The same URL generation logic was used across multiple flows — email confirmations, SSO redirects, registration links. The header injection poisoned all of them.
What the Fix Looks Like
The fix is straightforward: never use client-supplied headers to generate URLs in emails.
The server should use a hardcoded canonical domain from its configuration. Not from the request. Not from any header. A value that the application controls and that no external request can influence.
# Instead of:
url = f"https://{request.headers['X-Forwarded-Host']}/reset?token={token}"
# Use:
url = f"https://{settings.CANONICAL_DOMAIN}/reset?token={token}"
If the application runs behind a reverse proxy and needs to know the original host, the proxy should be configured to set a trusted header that the application explicitly allows — and the application should validate it against a whitelist of known domains.
The Takeaway
Email authentication (SPF, DKIM, DMARC) protects the sender. It confirms that the email actually came from who it claims to come from. It says nothing about the content of the email — including the links inside it.
A perfectly authenticated email can carry a perfectly malicious link if the backend that generated it trusted the wrong input.
The most dangerous attacks aren't the ones that look suspicious. They're the ones that look completely legitimate — because, technically, they are.