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.

Technical name: Reset token not validatedControlled demo · fake data

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.

  1. Request a password reset on your app, use the link, then open the same link again. It should fail.
  2. Request another reset and wait longer than the stated expiry before using it. It should fail.
  3. 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

Before
Reset your password
Sent 2 days ago EXPIRED
RESULT
Password changed

Expired link. Password changed anyway.

After
Reset your password
Sent 2 days ago EXPIRED
RESULT
Link expired. Request a new one.

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.

Fix it yourself

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 and after the fix
Same requestBeforeAfter
Expired linkAcceptedRejected
Fresh link, first useNot testedWorks
Same fresh link againNot testedRejected

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.

Learn more