Skip to content

Password reset stays timing-distinguishable for known addresses #812

Description

@stickerdaniel

requestPasswordReset answers identically for a known and an unknown address: same status, same body, no redirect, and a failing send is swallowed by runInBackgroundOrAwait. The remaining difference is how long the two take.

Better Auth mitigates the miss path with generateId(24) plus one dummy findVerificationValue lookup (node_modules/better-auth/dist/api/routes/password.mjs:50-62). The hit path instead writes a verification record and then awaits sendResetPassword (same file, lines 64-77), which in this app runs internal.emails.send.sendResetPasswordEmail: a locale query, a template render, and a Resend component mutation. No advanced.backgroundTasks.handler is configured, so runInBackgroundOrAwait awaits all of it (node_modules/better-auth/dist/context/create-context.mjs:211-220). One read against several extra round trips is a measurable gap, and it lets an attacker classify addresses that the response body deliberately does not distinguish.

Better Auth's per-IP rate limit does not vary by submitted address, so an attacker is throttled rather than blocked, which lowers the severity without closing the channel.

Scheduling the send with ctx.scheduler.runAfter(0, ...) narrows the gap but does not close it: the verification-record write remains, and moving the send out of the request changes what happens to a delivery failure and how it retries. That is a behaviour change worth its own review, which is why it is filed rather than folded into #809.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions