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.
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
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
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:
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.
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#editcontroller 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
Refererwill still contain the original password reset token, but the token is no longer valid.
- Update the
passwords#editcontroller action so it detects the presence of a token in the URL, stores that token in the session, and redirects back to
passwords#editwith the token removed the URL. The
Refererwill 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.