Skip to content

fix: don't send OTPs and links to banned users - #2900

Open
eliabieri wants to merge 1 commit into
supabase:masterfrom
eliabieri:fix/no-otp-for-banned-users
Open

eliabieri wants to merge 1 commit into
supabase:masterfrom
eliabieri:fix/no-otp-for-banned-users

Conversation

@eliabieri

@eliabieri eliabieri commented Nov 4, 2026 •

Copy link
Copy Markdown

What kind of change does this PR introduce?

Bug fix

What is the current behavior?

/magiclink, /otp (email and SMS), /recover and /resend never check whether the user is banned. They send the email or SMS as usual, even though the token in it can never be used: /verify and the token grants reject banned users with user_banned.

So a banned user can still request magic links, OTPs and recovery emails, and so can anyone who knows their email or phone number, using the public anon key. Each request costs email quota or SMS spend and affects sender reputation. The person receiving it gets a link that fails.

What is the new behavior?

When the user is banned, these handlers skip the send and return an empty 200. That is the same response /recover and /resend already give for an unknown email, so the response doesn't reveal that the account is banned.

The check runs right after the user lookup, before any other side effects. No flow state, token or audit log entry is created, and an unconfirmed user's password is not cleared.

Additional context

  • Same class of consistency fix as fix: reject AT from banned users #2642 (reject access tokens from banned users), chore: ensure consistent ban behaviour #2759 (ban check for admin credentials) and fix: reject banned users in the PKCE grant #2806 (ban check in the PKCE grant).
  • Tests: TestOtpBannedUser (magic link via /otp and /magiclink, SMS OTP via /otp), TestRecover_BannedUser and TestResendBannedUser (signup and email change). Each asserts that no token or *_sent_at timestamp is written while the user is banned, and that sending works again once the ban has expired.
  • Open question: a banned user now always gets 200, even where a non-banned user would get 429 from the per-user max frequency check. Someone who already knows the email could tell the two apart by sending two requests in a row. Happy to apply the frequency check before the ban check if you'd rather avoid that.
  • Not covered: /signup for an existing unconfirmed user still resends the confirmation email. Banned accounts are normally confirmed, so I left it out to keep this PR small. Happy to add it.

/magiclink, /otp, /recover and /resend sent emails and SMS to banned
users even though the token can never be redeemed, since /verify and
the token grants reject banned users. Skip the send and return the same
empty 200 as for an unknown user, so the ban status isn't revealed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eliabieri
eliabieri requested a review from a team as a code owner November 4, 2026 08:06

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant