An expired reset link still worked
In this controlled demo, an expired password reset link still works. The expiry time is saved, but the reset handler never checks it.
Demo built and tested 20 Sept 2026 · Published 23 Sept 2026 · Last reviewed 24 Sept 2026 · By Ctrl+Z Therapy (how we test)
Can an expired password reset link still work?
Yes, if the reset handler only checks that the token exists. Saving an expiry date does nothing unless the server compares it with the current time, and a token that isn't marked as used can be used again. The fix: reject expired or used tokens, and mark the token used in the same step as the password change.
How to tell if your app has this
No code needed.
- Request a password reset on your app, use the link, then open the same link again. It should fail.
- Request another reset and wait longer than the stated expiry before using it. It should fail.
- Ask your AI tool: "In my reset password handler, where do we check the token's expiry and whether it was already used?"
Before and after
Expired link. Password changed anyway.
Expired link rejected. A fresh link works once.
Why AI-built apps end up with this
The happy path only needs the token to exist, so that is what gets checked. Expiry and single use are rules nobody sees during normal testing, because you always click a fresh link once.
How do I fix it? Paste this into your AI tool
My password reset handler accepts a reset token without checking whether it expired or was already used. Change it so the server rejects expired or used tokens, and marks the token as used in the same database transaction as the password change, so it can only work once. Tokens should expire after a short time, for example 30 to 60 minutes.Works with Lovable, Bolt, Cursor, Replit and similar tools. Then run the check-up below on your own app.
How we checked the fix
| Same request | Before | After |
|---|---|---|
| Expired link | Accepted | Rejected |
| Fresh link, first use | Not tested | Works |
| Same fresh link again | Not tested | Rejected |
Results from our demo test, run against the same demo before and after the fix.
Common mistakes when fixing this
- An expiry date is just a number until the server enforces it.
Questions people ask
How long should a reset link last?
Short: minutes to an hour is common. The OWASP Forgot Password Cheat Sheet recommends short-lived, single-use tokens.
Why does marking the token used need to be atomic?
If two requests use the same token at the same moment, both can pass a separate check. Doing the check and the update in one database transaction makes sure it works only once.
Does Supabase or Firebase Auth handle this for me?
Their built-in reset flows manage their own tokens. This bug appears when an app builds its own reset flow and token table.
For developers: the cause and the fix in code
− if token_exists(token): + if unexpired and unused: + consume(token) # atomic
Simplified. Your stack will look different; the principle is the same.