[GH-ISSUE #8096] /change-email leaks account existence + sign-up synthetic user missing plugin fields #28316

Closed
opened 2026-04-17 19:44:39 -05:00 by GiteaMirror · 0 comments
Owner

Originally created by @nphlp on GitHub (Feb 22, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8096

Following #7972 and #8091, two gaps remain in the email enumeration protection:

1. /change-email still returns 422 for existing emails

The endpoint throws 422 USER_ALREADY_EXISTS when the target email is already registered. An authenticated user can enumerate all emails in the database by attempting to change their email.

Since /change-email always returns { status: true } in all normal branches (email updated, verification sent to old email, verification sent to new email), it can safely return { status: true } when the email already exists too — with simulated HMAC token generation for timing consistency.

2. Sign-up synthetic user doesn't match real user shape

The synthetic user built in #8091 has two issues:

  • Missing plugin fields: plugins that set fields via databaseHooks.user.create.before (e.g. admin sets role: "user") or have nullable fields without defaults (e.g. banReason, banExpires) are absent from the synthetic response.
  • Property order: the synthetic object spreads additionalUserFields first, then core fields — real users created by the DB adapter have a different Object.keys() order, with id injected last by transformOutput.

A customSyntheticUser callback gives the developer full control over the synthetic user object, including key order and plugin-specific fields.


I have a fix ready covering both points, with tests and updated documentation (email-password, admin plugin, security reference, options reference).

Originally created by @nphlp on GitHub (Feb 22, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8096 Following #7972 and #8091, two gaps remain in the email enumeration protection: ### 1. `/change-email` still returns 422 for existing emails The endpoint throws `422 USER_ALREADY_EXISTS` when the target email is already registered. An authenticated user can enumerate all emails in the database by attempting to change their email. Since `/change-email` always returns `{ status: true }` in all normal branches (email updated, verification sent to old email, verification sent to new email), it can safely return `{ status: true }` when the email already exists too — with simulated HMAC token generation for timing consistency. ### 2. Sign-up synthetic user doesn't match real user shape The synthetic user built in #8091 has two issues: - **Missing plugin fields**: plugins that set fields via `databaseHooks.user.create.before` (e.g. admin sets `role: "user"`) or have nullable fields without defaults (e.g. `banReason`, `banExpires`) are absent from the synthetic response. - **Property order**: the synthetic object spreads `additionalUserFields` first, then core fields — real users created by the DB adapter have a different `Object.keys()` order, with `id` injected last by `transformOutput`. A `customSyntheticUser` callback gives the developer full control over the synthetic user object, including key order and plugin-specific fields. --- I have a fix ready covering both points, with tests and updated documentation (email-password, admin plugin, security reference, options reference).
GiteaMirror added the locked label 2026-04-17 19:44:39 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#28316