Every account takeover researcher has seen this bug. It’s so consistent that it’s worth describing in full so you can check your own code.
The scenario
A user (Alice) and an attacker (Mallory) are sharing a browser session. Maybe it’s a cafe and Alice walked away. Maybe it’s a poorly isolated kiosk. Maybe it’s a more creative scenario. Mallory has access to Alice’s session but not her password.
Mallory:
- Goes to account settings
- Changes Alice’s email to
mallory@attacker.com - Clicks “forgot password”
- Receives the reset link at
mallory@attacker.com - Sets a new password
- Owns the account. Alice is locked out.
Every step is allowed by most apps. Let’s unpack each one.
The defense layer by layer
Require the current password to change the email
The email is a recovery vector. Changing it should require proving you know the current password (or holding a recently issued step-up-auth token). No current password, no email change.
POST /account/email
current_password: "..."
new_email: "..."
If the current password isn’t provided or is wrong, reject. Don’t be clever — the user can click a “change email” flow that prompts them once.
Email the old address first, with an undo window
After a successful email change, send a notification to the old email:
“Your account email was changed to j***@attacker.com. If this wasn’t you, click here to undo.”
The undo link has a 24-48 hour lifetime. If it’s used, revert the email, invalidate all sessions, and force a password reset. This is the safety net that catches the case where the current-password check was bypassed (session hijack after login, shoulder surfing, etc.).
Don’t invalidate existing sessions until the change is confirmed
During the undo window, keep the old email as the canonical account email in some form. Only invalidate sessions if the new email is verified AND the undo window passes. Otherwise, a compromised session can change the email and disconnect the legitimate user in one step.
Invalidate password reset tokens on email change
If an email change is in-flight (verification sent to new address, not yet confirmed), no password reset token should be issued to the new address until the change is confirmed.
Test it
Write an integration test:
- Sign in as A.
- Change email to B (without re-auth). Expect rejection.
- Change email to B with current password. Expect pending verification.
- Click “forgot password” before the verification completes. Expect the reset email to go to A, not B.
If any step fails, you’ve got work to do.