Skip to content

fix: mitigate timing-based user enumeration in password grant - #2693

Open
Mahaveer1013 wants to merge 2 commits into
supabase:masterfrom
Mahaveer1013:fix/password-grant-timing-leak
Open

fix: mitigate timing-based user enumeration in password grant#2693
Mahaveer1013 wants to merge 2 commits into
supabase:masterfrom
Mahaveer1013:fix/password-grant-timing-leak

Conversation

@Mahaveer1013

@Mahaveer1013 Mahaveer1013 commented Aug 10, 2026

Copy link
Copy Markdown

Summary

ResourceOwnerPasswordGrant only performed a bcrypt comparison when the
looked-up user existed and had a password set. The not-found and
no-password branches returned the invalid_credentials error immediately,
with no comparable work — making response time distinguishable from a real
failed-password attempt (~75-80ms gap at default bcrypt cost measured
against a hosted project) and disclosing whether an account exists.

  • Generate a fixed decoy password hash once at API startup, at the same
    bcrypt cost used for real user passwords (crypto.GenerateFromPassword,
    same call site pattern as internal/models/user.go).
  • Add compareDummyPasswordHash, which runs a bcrypt compare against that
    decoy hash and swallows the expected mismatch error.
  • Call it from both short-circuit branches in ResourceOwnerPasswordGrant
    (user not found, and user found but has no password) before returning
    the same invalid_credentials error, so timing no longer depends on
    which branch was taken.

No change to response status codes, error codes, or response bodies —
this only equalizes latency.

Closes #2674

Test plan

  • go build ./...
  • gofmt -l clean on changed files
  • go vet ./internal/api/...
  • go test ./internal/api/... -run TestToken -v -count=1 — all password grant tests pass, including new TestTokenPasswordGrantMissingUserChecksDummyPasswordHash, which corrupts the decoy hash and asserts the not-found branch now surfaces the comparison failure (500), proving the helper is actually wired into that branch.

ResourceOwnerPasswordGrant only ran a bcrypt comparison when a user
was found and had a password set. The not-found and no-password
branches returned immediately, making response time distinguishable
from a real failed-password attempt (~75-80ms gap at default bcrypt
cost), which discloses account existence.

Generate a fixed decoy hash at startup (same cost as real password
hashes) and compare against it on both short-circuit branches before
returning the same invalid_credentials error, so timing no longer
depends on whether the account exists.

Closes supabase#2674

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

Password grant leaks account existence via timing: no hash verify on the unknown-user path

1 participant