Skip to content
BuildThenTestedSee packages

Password reset links that never expire

An old reset email found in a shared inbox, a forwarded thread or a breached mailbox lets someone take over the account months later. How the app knows who you are and what you are allowed to do. Check it yourself with the steps below; our test adds what a script and a second account can find.

What it looks like

// reset-password.ts
const token = crypto.randomUUID();
await db.resetTokens.insert({ userId, token });   // no expires_at
await sendEmail(user.email, `${APP_URL}/reset?token=${token}`);

// /reset handler: looks the token up, sets the password,
// and never deletes it
A hand-written reset flow that stores a token (a one-off code that stands in for the password) with no expiry and does not remove it after use. Every reset email ever sent stays a working key to the account.

What happens

An old reset email found in a shared inbox, a forwarded thread or a breached mailbox lets someone take over the account months later.

Where it shows up

Descriptive, not a verdict: builders and stacks whose defaults make this reachable, or that a public incident names. Each hub states the tool's own security features.

Check it yourself

Uses your builder's dashboard and your browser. No tools to install.

  1. Request a password reset for your own test account and keep the email.
  2. Use the link to set a new password, then click the same link again.
  3. Request a second reset, wait a day, then open the day-old link.
  4. If your app uses Supabase Auth, open Authentication, then Providers, then Email, and read the Email OTP Expiration value.

A bad result is a used or day-old link still letting you set a password, or an expiry set to more than a day.

What we check

We generate several reset links, then replay used, aged and slightly altered tokens against the reset endpoint, and check whether tokens are stored in plain text in the database. The report states which replays succeeded and how the tokens are stored.

Fix direction

Let the auth provider run the reset flow where you can: Supabase's email links are one-time and expire after an hour by default, and its docs discourage settings above a day. If the flow is custom, OWASP's guidance is a random token that is single use, expires after a short period and is stored hashed.

Mechanism documented in: Forgot Password Cheat Sheet (verified 2026-09-21); Passwordless email sign-in (verified 2026-09-21).

Covered by

Pre-launch check ($490, 48 hours) and Security sweep ($290, 24 hours)

Both include a re-check after your fixes.

Compare packages

Related patterns

Questions

how long should a password reset link be valid

Long enough to open the email, no longer. OWASP's forgot-password guidance asks for tokens that are single use and expire after an appropriate period; Supabase's default for email links is an hour and it discourages anything above a day. Test your own app by clicking a used link and a day-old link.

does supabase password reset link expire

Yes. Supabase's docs state that the Email OTP Expiration setting governs magic links and other email links, including password recovery, and that links expire after an hour by default and are one-time use. The setting is under Authentication, then Providers, then Email. The risk appears when generated code bypasses Supabase Auth and writes its own reset flow.

can an old password reset email be used to hack an account

If the token never expires and is not deleted after use, yes: anyone who later reads that email can set a new password. Shared inboxes, forwarded threads and mailbox breaches are the usual routes. The self-check above, clicking a used link and an aged link, tells you in a few minutes whether your app has this gap.

Back to the catalogue, or run the self-check for your stack.