November 3, 2026
When a One-Time Password Works Twice: A Password Reset Logic Flaw
How replaying a completed password reset request revealed a missing security boundary.
By Aliso
4 min read
By Aliso (Ramji Singh) · In collaboration with Hacksparr0w
A password reset code makes a simple promise: use it once to prove ownership of an account, change the password, and the code loses its power.
During an authorized bug bounty assessment, I found a reset flow that did not enforce that final step. After a successful password change, the same verification code could be submitted again with a different password value. The server accepted the replay.
The report was accepted through YesWeHack and classified as High severity, with a CVSS score of 7.4, under Business Logic Errors (CWE-840).
This case study focuses on the behavior and the lesson behind it. Target identifiers and sensitive request values have been omitted. Testing was limited to my own account. The question behind the finding Password recovery is often tested by asking whether an incorrect code gets rejected. That is necessary, but it does not cover the entire lifecycle of a valid code.
My question was different: What happens if a correct code is submitted again after it has already authorized a password change?
A code can be correct and still be invalid for the current operation. It may have expired, been superseded, or already been consumed. The server needs to distinguish those states. Understanding the normal flow The application used an email-based recovery process. A user entered their account email, requested a verification code, completed verification, and chose a new password.
The final request carried three relevant pieces of information:
- The account identifier.
- The email verification code.
- The new password value in the representation generated by the client. The password field appeared to contain an encoded or hashed representation rather than plaintext. Its precise format was not the issue. The important question was whether the verification code still authorized another password change after the first reset succeeded. Replaying the completed operation Using an intercepting proxy, I captured the final password reset request for my test account and allowed it to complete normally.
The application returned a success response.
I then replayed the request, keeping the account and verification code unchanged while replacing the password field with a different client-generated value.
The server returned success again.
The report included two request-and-response examples approximately 12 seconds apart. Both used the same verification code, but supplied different password values. No fresh code was requested between them.
The following simplified example illustrates the relationship between the requests. The route and values are placeholders, not the original target details: Initial reset: POST /example/password-reset account= validateCode= password=
Replay after success: POST /example/password-reset account= validateCode= password= The expected result for the second request was rejection because the code had already been used. Instead, the application reported another successful reset. What the evidence establishes The key finding was reuse after successful consumption should have occurred.
That is a narrower and more useful claim than saying the code never expires. A replay shortly after the first reset demonstrates a failure to enforce single use; it does not establish the code's maximum lifetime.
The original report described repeated successful resets. The attached request pair documents the immediate replay behavior. It does not independently demonstrate indefinite validity, a bypass of every expiry condition, or access to arbitrary accounts without first obtaining a valid code.
Another important reporting distinction is that an HTTP 200 response alone is not proof of a changed password. The application-level response here also reported success. For the strongest end-to-end evidence, a reproduction should additionally verify that the second password works and the first no longer does. That login comparison is not shown in the attached request pair.
Keeping these boundaries clear makes the technical explanation more credible. Why reuse matters A password reset code temporarily grants authority to replace an account's password. Once that authority has been exercised, the code should stop working.
If it remains usable, someone who has obtained the code may retain the ability to submit another reset after the legitimate reset has completed, while the code remains accepted.
That creates a conditional account-security risk: changing the password may not end the authority of a previously exposed reset code. A subsequent unauthorized reset could also prevent the legitimate user from signing in with the password they just chose.
The precondition matters. This finding did not demonstrate a way to obtain another user's code, bypass email access, or guess codes without restriction. The flaw concerned what an already valid code could do after use. The likely implementation gap Without access to the server's source code, I could not confirm the exact internal implementation.
The observed behavior was consistent with a validation process that checked whether the submitted code matched an issued code, but failed to enforce its consumed state after a successful password change.
A secure reset decision needs to answer more than "does this code match?" It also needs to establish that the code belongs to the intended account and purpose, remains within its validity period, and has not already authorized a completed operation.
The final password-changing endpoint must enforce those conditions itself. How to address the flaw The central correction is to make successful use consume the reset authorization.
Code validation, password replacement, and code consumption should be coordinated so that two requests cannot both succeed using the same authorization. A short expiry period is useful, but it does not replace single-use enforcement: a code that works repeatedly for a few minutes is still reusable.
An alternative design can exchange a verified code for a narrowly scoped, single-use reset token. That token must also be consumed when the password changes. Moving the authorization into another token does not solve the problem unless its lifecycle is enforced.
A regression check should cover both sequential reuse and concurrent submissions. After one successful reset, another attempt using the same authorization should fail.
These are remediation recommendations, not a claim that a particular fix has been deployed. The supplied report export records acceptance but does not include a verified remediation retest. What I took away This finding came from examining what remained possible after an operation had succeeded.
That is a valuable habit when reviewing authentication workflows. A security token's lifecycle includes issuance, validation, expiration, replacement, and consumption. Testing only the happy path leaves several of those transitions unexplored.
For password recovery, one small question exposed the missing boundary:
After a one-time code has done its job, can it do that job again?