Pentest Files: Account Takeover Via Password Reset Token Disclosure

A critical flaw in a password reset API handed attackers a full account takeover in just two requests. See how our tester found it, how it works, and how to fix it.

Welcome to our Pentest Files series. Each post presents a real, interesting or dangerous finding one of our testers has identified during an actual engagement, so you can see the kinds of things our pentesters get up to, and take steps to prevent similar vulnerabilities in your own assets. Findings are taken from real reports and anonymised to protect client confidentiality.

The Finding

During a recent web application penetration test, our tester Lee uncovered a critical flaw in the application’s password reset flow. What made this one particularly striking was how straightforward the exploitation was: the API was handing the reset token directly back to whoever asked for it, no inbox access required.

Vulnerability type: Account Takeover via Password Reset Token Disclosure

Severity: Critical

Discovered by: Lee, OnSecurity

The Setup: How It Started

Lee was working through the authentication flows of a web application when he reached the password reset feature. It’s a part of almost every application, and one that’s easy to overlook from a security perspective. The surface area looks small: enter your email, get a link, reset your password. Simple.

But the security of that flow depends entirely on one assumption holding true: that the reset token only ever reaches the legitimate account owner, via their email inbox. If that assumption breaks anywhere, the whole thing unravels.

Lee fired up his proxy tool, submitted a valid email address to the reset endpoint, and watched the response come back. What he saw gave him pause. The API wasn’t just acknowledging the request. It was returning the reset token directly in the response body, right there in plaintext, visible to whoever had made the call.

The Exploit: Two Requests, Full Account Access

Step 1: Request a password reset for any known email address

An attacker sends a POST request to the reset endpoint with a target user’s email:

POST /api/auth/request-password-reset HTTP/2
Host: app.redacted.com
Content-Type: application/json

{"email":"[email protected]"}

Step 2: Collect the token from the API response

Instead of a simple acknowledgement, the API returns the reset token in plaintext:

{"message": "If an account exists with this email, you will receive password reset instructions.",
  "token": "8e8d6a18759f2b06f3ba43b8538c7cf707537d69226cf48f151e0816c2f99291"}

Notice the irony: the message says the user will receive instructions, implying they will arrive by email, while simultaneously handing the token to whoever made the request.

Step 3: Use the token to reset the password

The attacker submits the token directly to the reset endpoint alongside a new password of their choosing:

POST /api/auth/reset-password HTTP/2
Host: app.redacted.com
Content-Type: application/json

{  "token": "8e8d6a18759f2b06f3ba43b8538c7cf707537d69226cf48f151e0816c2f99291",
  "email": "[email protected]",
  "newPassword": "AttackerControlledPassword1!"}

Step 4: Account fully compromised

The server responds with a 200 OK, confirms the password has been reset, and returns a valid session token. The attacker is now logged in as the victim. No access to the victim’s email required. No social engineering. No phishing. Just two API calls.

Why This Worked

Two things had to be true for this exploit to succeed, and both were:

  1. The reset token was returned in the API response. Rather than being sent exclusively to the account owner’s email, the token was handed back to whoever called the endpoint, regardless of who they were.
  2. There was no out-of-band verification step. A valid token alone was sufficient to reset the password. There was no secondary check to confirm the requester was the legitimate account owner.

Bonus: Username Enumeration

There is an additional issue worth noting. Because the API only returns a token when the email address supplied belongs to a registered account, an attacker can use the presence or absence of a token in the response to determine whether any given email is registered on the platform. This turns the reset endpoint into an account enumeration tool, useful for building a target list before executing the takeover at scale.

Why This Is Dangerous

This is not a theoretical risk or an edge case requiring precise timing. Any attacker who can reach the API endpoint and supply an email address has a direct, reliable path to full account takeover.

Real-world impact scenarios include:

  • Complete account takeover at scale. An attacker could systematically reset passwords across any accounts whose email addresses they know or can enumerate, locking out legitimate users and seizing control of their accounts.
  • Exposure of sensitive personal and financial data. Depending on the application, compromised accounts could expose personal information, financial records, documents, or transaction histories.
  • Privilege escalation. If the application has administrative or elevated roles, attackers could target high-privilege accounts and gain access to functionality far beyond that of a standard user.
  • Business fraud and financial loss. In applications that handle payments or approvals, account takeover can directly enable fraudulent transactions.
  • Reputational and regulatory damage. A breach of this nature, particularly involving personal or financial data, can carry significant consequences under data protection regulations such as GDPR.

The Remediation

The fix here is straightforward in principle, though it is worth reviewing the entire authentication flow while you are at it. The client received clear remediation guidance and was able to validate their fixes through a free retest via OnSecurity’s platform.

Never return a password reset token in an API response. The token should be generated server-side, stored securely, and transmitted only via a trusted out-of-band channel: an email sent to the account owner’s registered address. The API response to a reset request should contain nothing more than a neutral acknowledgement, regardless of whether the email is recognised.

Additional hardening steps:

  • Make tokens single-use. Once a reset token has been used or has expired, it should be immediately invalidated server-side.
  • Apply a short expiry window. Reset tokens should expire after a brief period, typically 15 to 60 minutes, to limit the window of opportunity for exploitation.
  • Return consistent responses regardless of whether the email exists. To prevent username enumeration, the API should return an identical response whether the email is registered or not, with no token in either case.
  • Rate-limit reset requests. Restrict the number of reset requests that can be made from a single IP address or for a single account within a given timeframe.
  • Log and alert on suspicious activity. Flag unusual volumes of password reset requests for review. This can help detect automated abuse early.
  • Review all authentication flows. Use this finding as a prompt to audit any other endpoints that handle credentials, tokens, or sensitive identifiers.

What This Looks Like in the Real World

Password reset flows are easy to get wrong. They involve temporary credentials, time-sensitive logic, and assumptions about out-of-band delivery that don’t always hold up under scrutiny. A small implementation mistake, returning a token in a response rather than exclusively via email, is enough to completely bypass the security model the feature is meant to enforce.

This finding was discovered during a structured, time-boxed penetration test. Without that test, this critical vulnerability could have remained undetected indefinitely, quietly waiting to be found by someone with far less benign intentions.

Lee identified and reported this finding in real time through OnSecurity’s platform, meaning the client’s team were able to see the issue, understand its impact, and begin remediation work while the test was still in progress, not weeks later when a PDF arrived in their inbox.

How to Protect Yourself

If your application has a password reset flow, it is worth asking:

  • Does the reset token appear anywhere in the API response?
  • Could an attacker reset any account password without access to that account’s email inbox?
  • Does your API return different responses depending on whether an email is registered?

If you are not certain of the answers, a penetration test is the most reliable way to find out before an attacker does.

Want to know if your application is vulnerable? Get a quote from OnSecurity

Related Articles

Pentest Files: How A Single HTTP Header Unlocked Every Customer’s Data

A single HTTP header. Fully client-controlled. Trusted completely by the server. In this Pentest Files, Daniel shows how modifying one value in a routine API request was enough to pull user data from every organisation on a multi-tenant SaaS platform, no special privileges required, no complex exploit chain, just a for loop and an integer.

The OnSecurity platform is currently experiencing issues. Our team is actively working to resolve this. Please try again shortly.