Skip to content

POST /auth/v1/recover returns email_address_invalid for an existing user and success for a missing user (account enumeration) #2702

Description

@EdmarSantos42

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.

  1. Create a confirmed user via Admin API, e.g. user@example.com.
  2. With the publishable key only, call auth.resetPasswordForEmail('user@example.com').
  3. Observe 400 / email_address_invalid.
  4. Call auth.resetPasswordForEmail('missing@example.com').
  5. Observe success with no error.
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions