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.
requestPasswordResetanswers identically for a known and an unknown address: same status, same body, no redirect, and a failing send is swallowed byrunInBackgroundOrAwait. The remaining difference is how long the two take.Better Auth mitigates the miss path with
generateId(24)plus one dummyfindVerificationValuelookup (node_modules/better-auth/dist/api/routes/password.mjs:50-62). The hit path instead writes a verification record and then awaitssendResetPassword(same file, lines 64-77), which in this app runsinternal.emails.send.sendResetPasswordEmail: a locale query, a template render, and a Resend component mutation. Noadvanced.backgroundTasks.handleris configured, sorunInBackgroundOrAwaitawaits 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.