A user changed their email and lost the account: how to fix it
Revert the email address of the affected account, end all sessions of that account, and turn on a confirmation step on both the old and the new address. An email change is an account takeover when the app accepts the change without proof from the account owner. The attacker then asks for a password reset and the reset link goes to the new address.
This guide shows how the attack works, how to confirm that your app has the gap, and how to close it. It also shows how to check the damage in your logs.
What does “user changed email, account takeover” mean?
The email address of an account is the recovery channel. Most apps send the password reset link to it. Whoever reads that mailbox can set a new password.
An attacker uses this fact in a short chain:
- The attacker gets access to a session of the user. The attacker can steal a token, use a shared computer, or use a cross-site scripting (XSS) bug.
- The attacker opens the profile page or calls the API and changes the email to an address that the attacker owns.
- The attacker starts a password reset. The link goes to the new address.
- The attacker sets a new password and signs in. The real user cannot recover the account, because the recovery address is now wrong.
A stolen session alone is a temporary problem. The user can sign out and the session ends. The email change makes the access permanent. For this reason, the email change is the step that you must protect.
Vibe coding tools often create this gap. You ask for a “profile settings” page with an email field. The tool writes a form that updates the email and shows a success message. The tool does not add a confirmation, a password check, or a notice to the old address.
Which flaws allow the takeover?
Four flaws appear most often. Your app can have more than one.
| Flaw | What the attacker does |
|---|---|
| No confirmation on the new address | Changes the email to an address that is mistyped or not owned by the attacker. Some apps then deliver reset mail to the wrong person. |
| No confirmation on the old address | Changes the email with only a session. The real user gets no chance to stop it. |
| No re-authentication | Uses a stolen session. The app does not ask for the current password or a fresh code. |
| Direct write to a profile table | Sends an update to the profiles table that holds a copy of the email. The app uses that copy for login or reset. |
The fourth flaw is easy to miss. Many apps store an email column in a public profiles table. If a row policy lets the user update any column, the user can change that copy. If the app trusts that copy for password reset, the attacker controls the reset address without any confirmation. See the Supabase RLS guide for how to restrict the columns that a user can write.
How do I confirm that my app has the flaw?
Test with two accounts that you own. Do not test on real user accounts.
- Create a test account A with an email address that you control. Create a second mailbox B.
- Sign in as A and open the profile page.
- Change the email to the address of mailbox B.
- Watch what happens. Note whether the app changes the address at once or waits for a confirmation.
- Check mailbox A and mailbox B. Count how many confirmation emails each one received.
- Sign out. Try to sign in with the new address and the old password.
Use this table to read the result:
| Result | Meaning |
|---|---|
| The email changes at once | The app has no confirmation. Fix it now. |
| Only mailbox B gets a link | The old address has no say. A stolen session is enough. |
| Both mailboxes get a link and the change waits | The flow is correct for this check. |
| The app asks for the current password | Re-authentication works. Check the API in the next step. |
Then test the API directly, because the form is not the only way in. Open DevTools, find the request that the form sends, and copy it as a curl command. Remove the password field from the request and send it again. If the server accepts the change, the check exists only in the browser.
Last, test the profile table. If your app uses Supabase, send a direct update to the profiles table with the session token of account A:
curl -X PATCH "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?id=eq.USER_A_ID" \
-H "apikey: YOUR_PUBLISHABLE_KEY" \
-H "Authorization: Bearer USER_A_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{"email":"mailbox-b@example.com"}'
If the response contains the new email, any user can rewrite that column. Fix the policy in the section below.
What do I do first? Fix it now, in order of urgency
Warning: Do not send mail to the new address that the attacker set. The mail can contain a reset link or a notice that helps the attacker. Contact the real user through another channel.
- Find the affected accounts. Use the damage checks in the next section. Make a list of every account whose email changed without a confirmation.
- Lock each affected account. Set a flag in your database or ban the user in your auth provider. The attacker then cannot sign in during the repair.
- Revert the email address. Restore the old address from your logs or from a backup. Do this for each account on your list.
- End all sessions of the account. A session token that the attacker holds can stay valid after the email change. In Supabase, call
supabase.auth.signOut({ scope: "global" })for the signed-in user, or ban the user for a short time and then remove the ban. Other auth providers have a “revoke all sessions” option. Use it. - Reset the password. Send the reset link to the reverted address only. Tell the user to choose a new password.
- Turn on confirmation on both addresses. See the next steps.
- Add a re-authentication step. The user must enter the current password, or a fresh one-time code, before the app starts an email change.
- Send a notice to the old address. The notice must name the new address and contain a link that cancels the change.
- Deploy the fix and repeat the test. Run the six test steps again with the two test accounts.
How do I turn on confirmation on both addresses in Supabase?
The Supabase CLI configuration reference lists the setting auth.email.double_confirm_changes. The description says: “If enabled, a user will be required to confirm any email change on both the old, and new email addresses.” The default is true.
Check the value in your project. In the dashboard, open the authentication settings for the email provider. If your project uses a config.toml file, check this block:
[auth.email]
double_confirm_changes = true
enable_confirmations = true
The second line, enable_confirmations, makes new users confirm their email before the first sign-in. The reference lists its default as false. Set it to true for a production app.
Supabase also lists auth.email.secure_password_change. The description says that the setting requires the current password when the user changes to a new password. Use it. A password change is the second step of many takeover chains.
Note: These settings protect the Supabase Auth flow. They do not protect a custom endpoint that writes to your own users or profiles table. Check those separately.
How do I stop users from writing the email column?
Do not let a user write the email in the profiles table. Keep one source of truth. The email lives in the auth system. If you need a copy, fill it with a database trigger and make the column read-only for users.
The simplest policy limits which columns the client can update:
revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url) on public.profiles to authenticated;
The first line removes the update right on the whole table. The second line gives back the right on two named columns. The email column is not in the list, so the API rejects a change to it. Keep your row policy as well, so that each user can only update their own row.
If your app uses another backend, apply the same rule on the server. Accept only a fixed list of fields from the request body. This also stops mass assignment. See mass assignment for the pattern.
What should the safe flow look like?
Use this sequence for every email change:
- The user is signed in and enters the current password again.
- The user enters the new address.
- The app stores the new address as a pending value. The account keeps the old address.
- The app sends a confirmation link to the new address.
- The app sends a second link and a notice to the old address.
- The app changes the email only after both links are used within a short time.
- The app ends all other sessions after the change.
The OWASP Authentication Cheat Sheet describes the same design. Without MFA, it requires the current password and sends confirmation emails to both the current and the proposed address. With MFA, it sends a notice to the current address and a confirmation to the new one.
How do I check the damage?
Look for email changes that no confirmation supports. Use these checks in order.
- Read the Auth logs. In the Supabase dashboard, open the logs for the Auth service. Search for user update events in the period that you suspect. Note the user ID, the IP address, and the time.
- Query the user table. Run a query in the SQL editor that lists recently updated users:
select id, email, email_confirmed_at, last_sign_in_at, updated_at
from auth.users
where updated_at > now() - interval '30 days'
order by updated_at desc;
- Compare with your own records. Compare each email with the address that you have in your billing system, your support tickets, and your newsletter list. A mismatch is a strong sign of a change that the user did not make.
- Look for password resets that follow a change. Find accounts where a password reset happens soon after an email change. This is the pattern of the attack.
- Check sign-ins from new places. Look for a new IP address or country after the change. Compare it with the earlier sign-ins of the same user.
- Check what the account did. Read the actions of the affected account after the change. Look for data exports, API key creation, role changes, and payment changes.
Caution: The auth.users table is not a full audit log. It shows the current state, not every past value. If you did not store the old email, use backups and mail logs to find it.
If you keep no logs, start now. Store the user ID, the old address, the new address, the IP address, and the time for each change.
How do I stop the next email change attack?
Use these controls to make the flow safe in the long term.
- Confirm on both addresses. The old address owner must agree to the change.
- Require re-authentication. Ask for the current password or a fresh code before the change starts.
- Send a notice with a cancel link. The real user gets a way to stop the change.
- End other sessions after sensitive changes. Apply this rule to email changes and password changes.
- Limit the writes. Make the
emailcolumn read-only for clients. Accept only named fields in every update endpoint. - Add MFA. A second factor makes a stolen session much less useful. The OWASP guide calls MFA the best defense against most password-based attacks.
- Test the flow after each release. AI tools can rewrite the profile page later and remove a check that worked before.
For the password reset side of the chain, read auth flows: magic link, OTP and reset. It shows how reset flows fail in the same way.
When should I get help?
Get help in these cases:
- You find more than a few affected accounts. A large number of changes can mean that someone found the flaw and used a script.
- The attacker used a session token and you do not know how the token leaked. An XSS bug or a leaked token can give the attacker access to more than one account.
- The affected accounts hold payment data or personal data. You may have a legal duty to report the breach. Ask a security engineer or a lawyer.
- Your own admin accounts were changed. Treat this as a full compromise. Rotate all secrets and review all admin actions.
After you finish, repeat the two-account test. Confirm that the app changes the address only after both links are used, and that the old address receives a notice.
Common questions
Why is an email change an account takeover?
Is a stolen session enough to take over an account?
Does Supabase confirm email changes on both addresses?
Should I send an email to the old address?
Can I stop the attack by blocking the email field in the profile form?
Test your email change flow
A VibeEval scan checks the sign-in and account flows of your deployed app and reports weak points that let a session or a request change account identity.
14-day free trial · No credit card · Cancel anytime