In Depth: Stealing Password Tokens with Forwarded Host Poisoning

[InDepth#13] User input comes in many different forms, and sometimes your app will believe whatever your users tell it... especially if it's in a header!

In Depth: Stealing Password Tokens with Forwarded Host Poisoning

Our story starts on the 1st January 2023, when I was pinged on Twitter:

Ankur Kumar linked me to a video by Daniel Coulbourne, covering a vulnerability in the core Laravel framework that was reported to them through their Bug Bounty program.

To save you clicking through, here is the video:

This vulnerability is a Host Header attack technique known as Password Reset Poisoning. The idea is to poison or spoof the values in specific request headers, to trick the server into believing the application is running as a specific host, causing it to generate links for the poisoned hostname rather than the real hostname.

In most cases, overriding the hostnames used for generated URLs won’t have much effect - the links are returned to the browser, where they can be modified anyway. The vulnerability occurs when links are generated and sent to someone else (i.e. the target), such as in an email. This allows the attacker to send a link to a malicious app, rather than the actual app - allowing them to steal any tokens or parameters from the URL. In this case of a password reset link, this token gives the attacker full access to hijack the targeted user’s account!

Before we go on, I want to make it clear that Laravel apps are not vulnerable by default.

However, it is common to run Laravel apps behind a reverse proxy (such as a CDN, caching layer, web application firewall (WAF), etc), and if you fail to properly configure both the TrustProxies and TrustHosts middleware, then your app may be vulnerable!

How It Works

Let’s break the attack down to understand how it works.

There are two requirements that make your app vulnerable:

  1. The TrustProxies middleware is enabled and configured to trust all proxies.
    i.e. $middleware->trustProxies(at: '*'); on Laravel 11.
  2. The TrustHosts middleware is disabled. (Which it is by default.)

This is a pretty common configuration when your app is behind a proxy. The app is configured to trust everything and TrustHosts is left disabled and ignored.

Once those conditions are met, the attack works like this:

  1. The attacker loads the Forgotten Password form, as per normal.
    GET https://example.com/forgot-password
  2. The server generates the form and sends it back in the response, as per normal. (This is needed to obtain the CSRF token.)
  3. The attacker submits the form, and in the process intercepts the request and injects a new header into the request:
    POST https://example.com/forgot-password
    X-Forwarded-Host: evilhacker.dev
    _token=<csrftoken>&email=victim@example.com
  4. Within the single request, the server:
    1. Sees the X-Forwarded-Host: evilhacker.dev header and tells itself to generate all links with that hostname.
    2. Generates the reset token, builds the link, and sends it to the victim.
      https://evilhacker.dev/reset-password/<token>?email=victim@example.com
  5. The victim receives the “Password Reset” email, recognises the app it was sent from and clicks the link.
  6. The victim visits the evilhacker.dev app, which records the reset token and immediately visits the real Reset Password form at example.com and submits the form to reset the user’s password. (This is trivial to automate.)

At this point the user has lost complete control over their account.

Why It Is So Serious

It’s easy to underestimate the effectiveness of this attack. It’s sending an email to complete a user-initiated action, so why would the victim click the link?