Describe the bug
On hosted Auth, POST /auth/v1/recover (resetPasswordForEmail with the publishable key) returns distinguishable public results for two syntactically valid addresses on the same reserved domain:
- existing confirmed user
@example.com → HTTP 400, email_address_invalid
- missing address
@example.com → success (ok, no error)
That is an account-existence oracle. Anyone with the publishable key can call the hosted endpoint directly; wrapping the call in an application server action does not close it.
This is not the old "User not found" leak for missing accounts (fixed in #338). Missing addresses already succeed. The leak is the existing-user path returning email_address_invalid while the missing path does not.
Related but different: #2398 (rate-limit / timing on recover). This report is a response-content difference (400 email_address_invalid vs success), not the 60-second cooldown oracle.
The Auth error catalog describes email_address_invalid as example/test domains not being supported. That does not match the observed behaviour: the missing @example.com address is accepted. The reserved-domain check appears to apply only when a user already exists.
To Reproduce
Environment: hosted project, Email provider enabled, public signup disabled (disable_signup = true), custom SMTP not configured. No production or real-person data.
- Create a confirmed user via Admin API, e.g.
user@example.com.
- With the publishable key only, call
auth.resetPasswordForEmail('user@example.com').
- Observe
400 / email_address_invalid.
- Call
auth.resetPasswordForEmail('missing@example.com').
- Observe success with no error.
- Delete the fixture user.
Signup stays disabled. CAPTCHA / recover rate limits may slow abuse; they do not remove the binary signal.
Expected behaviour
Public recover responses for a syntactically valid existing address and a syntactically valid missing address should be identical (same status, same error surface). A reserved-domain policy, if any, should apply before existence is distinguishable — not only on the existing-user send path.
Additional context
- Client:
@supabase/supabase-js resetPasswordForEmail → /auth/v1/recover
- Docs consulted (2026-08-16): resetPasswordForEmail, generateLink, Auth CAPTCHA, recover rate limits, Send Email Hook,
email_address_invalid
- No secrets, project refs, or real email addresses are included in this report
- User enumeration is understood to be outside the VDP; this is filed as Auth hardening on the hosted recover endpoint
Describe the bug
On hosted Auth,
POST /auth/v1/recover(resetPasswordForEmailwith the publishable key) returns distinguishable public results for two syntactically valid addresses on the same reserved domain:@example.com→ HTTP 400,email_address_invalid@example.com→ success (ok, no error)That is an account-existence oracle. Anyone with the publishable key can call the hosted endpoint directly; wrapping the call in an application server action does not close it.
This is not the old "User not found" leak for missing accounts (fixed in #338). Missing addresses already succeed. The leak is the existing-user path returning
email_address_invalidwhile the missing path does not.Related but different: #2398 (rate-limit / timing on recover). This report is a response-content difference (
400 email_address_invalidvs success), not the 60-second cooldown oracle.The Auth error catalog describes
email_address_invalidas example/test domains not being supported. That does not match the observed behaviour: the missing@example.comaddress is accepted. The reserved-domain check appears to apply only when a user already exists.To Reproduce
Environment: hosted project, Email provider enabled, public signup disabled (
disable_signup = true), custom SMTP not configured. No production or real-person data.user@example.com.auth.resetPasswordForEmail('user@example.com').400/email_address_invalid.auth.resetPasswordForEmail('missing@example.com').Signup stays disabled. CAPTCHA / recover rate limits may slow abuse; they do not remove the binary signal.
Expected behaviour
Public recover responses for a syntactically valid existing address and a syntactically valid missing address should be identical (same status, same error surface). A reserved-domain policy, if any, should apply before existence is distinguishable — not only on the existing-user send path.
Additional context
@supabase/supabase-jsresetPasswordForEmail→/auth/v1/recoveremail_address_invalid