Self-service password resets are a common part of many web applications. The typical password reset link is emailed to the user and contains a unique token that in some manner identifies the user. By clicking the link, the user proves they have access to the email associated to the account, and has now authenticated using a second factor. At this point, they are asked to provide a new password.

If an attacker were able to access the password reset link, the attacker would then be able to authenticate as the user and provide that new password, thus gaining unfettered access to the user’s account. Attackers frequently accomplish this by gaining access to the user’s email, though as I recently discovered many applications are unwittingly passing password reset links to third party sites. To understand how, we must first understand how websites gather referrer data.

The HTTP Referer header (the misspelling of “referrer” is an unfortunate mistake of history that I begrudgingly replicate here when referring to the header) is sent to the server by your browser and identifies the URL of the requesting site. It’s chiefly used by site operators for the purposes of analytics. Referrer data is how site operators know which search terms people are using to find their site, or how many visits their latest social media blitz drove. The browser sends this header when users click links or when the browser fetches a resource such as an image, a stylesheet, a JavaScript file, or a video that is referenced in the document being rendered.

Browsers will not send the Referer for resources fetched via HTTP from a document loaded via HTTPS. Additionally, some browsers respect aspects of the referrer policy spec, which allows authors to control the conditions under which the full referrer will be revealed in the Referer . Unfortunately, support for this spec is not yet universal and it should not be solely relied on for security concerns.

When the user requests a password reset, they are emailed a link that looks something like this: https://example.com/passwords/edit?token=1234abcd . When the user clicks that link, the application renders the password reset form inside the usual site layout, which may contain references to assets loaded from a trusted content delivery network (CDN) or an analytics package such as Segment. The layout may also contain links to external sites such as the company’s social media profiles.

Barring an intermediate redirect between the user clicking the link in their email and the password reset form being rendered, the browser will expose the password reset link in the Referer sent when requesting the referenced assets or the analytics package. If the user does not complete the password reset and instead clicks on one of the external links, the password reset link will be leaked via the Referer on those requests as well.

The seriousness of this leak is mitigated by several factors:

Most password reset tokens are invalidated once the user completes the password reset form and thus the window for using a leaked password reset link is likely to be pretty small.

Barring user generated content being improperly rendered on the password reset page, an attacker cannot control which sites the token is leaked to.

If the token is leaked, it is most likely to be leaked to a site that you consider to be a trusted partner.

These factors make the leak something that is unlikely to be easily exploited. While it’s hard to imagine a nefarious employee at your trusted CDN waiting to act immediately on incoming referrer data, it’s less hard to imagine an attacker targeting that same CDN and stumbling on this potentially valuable information in server logs. For that reason, it’s important to address this issue even if the likelihood of an immediate exploit is low.

You can perform the following test on your own site to see if it is possible for it to leak working password reset links to third party sites.

Request a password reset and click the link that is emailed to you. Copy the URL from the resulting page. Open a private browser window or a different web browser and paste the copied URL.

If a working password reset form is rendered then you have proven that without any mitigating steps, any Referer generated by this page would contain a working password reset URL.

You can use the networking tab in your browser’s web inspector to inspect all of the requests generated by the page to see if any of them leaked the password reset link externally in the process of rendering the page. You can inspect all HTML links in the document to see if any of them point to an external HTTPS resource and thus would leak the password reset link if clicked.

Here we see Upcase leaking password reset links to its CDN, Cloudfront.

If the page loaded in your private browser instance, but it does not fetch or link to any external resources then it is not currently leaking the password reset link. However, you should still take steps to eliminate this possibility altogether as any future changes to, for instance, footer links or external resources loaded could cause this to be a problem.

This leak can be plugged by ensuring that the URL of the page that is rendered as a result of clicking the password reset link does not contain a valid password reset token. When this issue was reported and fixed in Clearance, our authentication engine for Rails, we considered the following two fixes:

Update the passwords#edit controller action so it immediately invalidates the password reset token in the request and generates a new one that is used in the form action. The Referer will still contain the original password reset token, but the token is no longer valid. Update the passwords#edit controller action so it detects the presence of a token in the URL, stores that token in the session, and redirects back to passwords#edit with the token removed the URL. The Referer will no longer contain the necessary token.

We went with the session-based approach in Clearance 1.15.0 because we felt the immediately expiring password reset link in the first approach broke idempotent get requests. You can see how each of these approaches changed our code and the discussion of them both in the pull requests I submitted for the session-based approach and the immediate expiration approach. If you use Clearance, you can update to Clearance 1.15 or newer to fix this issue.

If you’d like to hear more about the evolution of this issue and it’s fix in Clearance, you can listen to episodes 81 and 82 of The Bike Shed, a podcast about web development.

Thank you to Aditya Prakash for bringing the issue with Clearance to my attention and to Jeroen Visser for assisting in defining its scope.