The reset flow is one of the most security-sensitive parts of an app, because its entire job is to let someone set a new password without knowing the old one. Every weakness in it is a direct route to account takeover. AI tools build a reset that works, you enter an email, you get a link, you set a new password, and skip the parts that make it safe. Here are the four holes we see most.
1. Guessable reset tokens
The token in the reset link has to be unpredictable, or an attacker can guess a valid one. Generated with Math.random or a short sequence, it is exactly that. Use a long, cryptographically secure random value:
// weak: predictable
const token = Math.random().toString(36).slice(2);
// strong: unguessable
import crypto from "node:crypto";
const token = crypto.randomBytes(32).toString("hex");This is the same problem as using Math.random for anything security-related.
Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.
See a live scan →2. Tokens that never expire or can be reused
A reset token should work once and only for a short window. If it never expires, a link that leaks a year from now still works. If it is reusable, a single intercepted link is a permanent key. Give it a short expiry and invalidate it on use or when a new one is requested. Store only a hash of the token, so a database leak does not hand out valid tokens.
// store a hash + an expiry, not the raw token
const hash = crypto.createHash("sha256").update(token).digest("hex");
await db.resetTokens.create({
userId, tokenHash: hash,
expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour
});
// on use: look up by hash, check not expired, then delete it3. User enumeration
If the reset page responds differently for a registered email than an unknown one, an attacker learns which emails have accounts. Return the same neutral message either way, and send the email only when the account actually exists:
// leaks which emails are registered
if (!user) return res.json({ error: "No account with that email" });
// neutral: reveals nothing either way
if (user) await sendResetEmail(user);
return res.json({ message: "If an account exists, we sent a reset link." });4. No rate limiting
Both the request-reset and the token-submission endpoints need a rate limit, or an attacker can enumerate users, spam a victim, or try to brute-force a short token. Key it on the real client IP and the account. The pattern is in adding rate limiting to a login, and it applies to reset the same way.
Frequently asked questions
Why is the password reset flow such a common weak spot?
Because it is a deliberate way to take over an account, so every shortcut in it is a way in. A reset flow issues a token that lets someone set a new password without knowing the old one. If that token is guessable, never expires, or the flow leaks which emails are registered, you have handed an attacker a path to any account. It gets less attention than login, which is exactly why the holes survive.
What makes a reset token secure?
It should be long, generated with a cryptographically secure random source, single-use, and short-lived. Use crypto.randomBytes, not Math.random, so it cannot be predicted. Store a hash of it, not the token itself, so a database leak does not expose valid tokens. Expire it quickly, an hour is common, and invalidate it the moment it is used or a new one is requested.
What is user enumeration and why does it matter here?
User enumeration is when your app reveals whether an email is registered. If the reset page says 'no account with that email' for unknown addresses and 'reset sent' for known ones, an attacker can discover which emails have accounts, which fuels targeted attacks. The fix is to return the same neutral response either way: 'if an account exists, we sent a link.' Send the email only when the account is real, but never let the response reveal that.
Do I need rate limiting on the reset endpoint?
Yes. Without it, an attacker can hammer the reset request endpoint to enumerate users or spam a victim, and if tokens are short they could try to brute-force them. Rate limit both the request-reset endpoint and the token-submission endpoint, keyed on the real client IP and, where relevant, the account. It is the same protection every sensitive endpoint needs.
Check your reset flow
The reset flow touches several of the patterns a scan looks for, weak randomness, missing rate limiting, and more. Reviewing it as its own thing is worth it because the stakes are account takeover. Run a free scan and check what your app exposes, then walk your reset flow against these four points.