[GH-ISSUE #7944] Security: email OTP endpoint leaks account existence when signup is disabled (USER_NOT_FOUND) #19581

Closed
opened 2026-04-15 18:50:58 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @ivan-dalmet on GitHub (Feb 12, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/7944

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

When account creation/sign-up is disabled, calling authClient.emailOtp.sendVerificationOtp(...) returns USER_NOT_FOUND. This allows an attacker to infer whether an email is registered (account enumeration).

Config

betterAuth({
  plugins: [
   emailOTP({
      disableSignUp: true, 
   })]
})

Usage

await authClient.emailOtp.sendVerificationOtp({
  email: "non-existing@example.com",
  type: 'sign-in',
})

Online demo

Current vs. Expected behavior

Current

API returns whether a user exists or not.

Why this is a security issue
OTP / magic-link / password reset style endpoints should not reveal whether a user exists. Returning USER_NOT_FOUND (or any distinguishable error/status/response) enables account enumeration and targeted attacks (phishing, credential stuffing, harassment).

Expected

API should not reveal whether a user exists.

What version of Better Auth are you using?

1.4.17

System info

Deployed on vercel 

{
  "node": {
    "version": "v22.19.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "9.7.2"
  },
  "frameworks": [
    {
      "name": "react",
      "version": "19.2.4"
    }
  ],
  "databases": [
    {
      "name": "@prisma/client",
      "version": "6.19.2"
    }
  ]
}

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

betterAuth({
  database: prismaAdapter(db, {
    provider: 'postgresql',
  }),
  user: {
    additionalFields: {
      onboardedAt: {
        type: 'date',
      },
    },
  },
  onAPIError: {
    throw: true,
    errorURL: '/login/error',
  },
  plugins: [
    emailOTP({
      disableSignUp: true,
    }),
  ],
});

Additional context

No response

Originally created by @ivan-dalmet on GitHub (Feb 12, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/7944 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce When account creation/sign-up is disabled, calling `authClient.emailOtp.sendVerificationOtp(...)` returns `USER_NOT_FOUND`. This allows an attacker to infer whether an email is registered (account enumeration). ## Config ```ts betterAuth({ plugins: [ emailOTP({ disableSignUp: true, })] }) ``` ## Usage ```ts await authClient.emailOtp.sendVerificationOtp({ email: "non-existing@example.com", type: 'sign-in', }) ``` ## Online demo - Go to https://demo.start-ui.com/login - Login with `nonexisting@email.com` - Use `000000` code - API returns `{"code":"USER_NOT_FOUND","message":"User not found"}` ### Current vs. Expected behavior ## Current API returns whether a user exists or not. **Why this is a security issue** OTP / magic-link / password reset style endpoints should not reveal whether a user exists. Returning `USER_NOT_FOUND` (or any distinguishable error/status/response) enables account enumeration and targeted attacks (phishing, credential stuffing, harassment). ## Expected API should not reveal whether a user exists. ### What version of Better Auth are you using? 1.4.17 ### System info ```bash Deployed on vercel { "node": { "version": "v22.19.0", "env": "development" }, "packageManager": { "name": "npm", "version": "9.7.2" }, "frameworks": [ { "name": "react", "version": "19.2.4" } ], "databases": [ { "name": "@prisma/client", "version": "6.19.2" } ] } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript betterAuth({ database: prismaAdapter(db, { provider: 'postgresql', }), user: { additionalFields: { onboardedAt: { type: 'date', }, }, }, onAPIError: { throw: true, errorURL: '/login/error', }, plugins: [ emailOTP({ disableSignUp: true, }), ], }); ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-15 18:50:58 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Feb 12, 2026):

Thank you for bringing this security concern to our attention! It appears there is indeed an issue here where the authClient.emailOtp.sendVerificationOtp(...) method returns USER_NOT_FOUND when account creation is disabled. This behavior can allow an attacker to discern whether an email exists in the system, leading to potential account enumeration vulnerabilities.

To address this issue, you may consider implementing a more generic response for the OTP endpoint, such as returning a generic message that does not inform users whether the email exists. This would help mitigate the risk of account enumeration.

Additionally, you might want to tag the Better Auth team members, Beka and Max (@bekacru and @ping__), to ensure they are aware of this issue and can provide further insights or potential fixes.

What further steps are you considering to enhance your authentication security? Feel free to ask if you have more questions!

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3891055274 --> @better-auth-agent[bot] commented on GitHub (Feb 12, 2026): Thank you for bringing this security concern to our attention! It appears there is indeed an issue here where the `authClient.emailOtp.sendVerificationOtp(...)` method returns `USER_NOT_FOUND` when account creation is disabled. This behavior can allow an attacker to discern whether an email exists in the system, leading to potential account enumeration vulnerabilities. To address this issue, you may consider implementing a more generic response for the OTP endpoint, such as returning a generic message that does not inform users whether the email exists. This would help mitigate the risk of account enumeration. Additionally, you might want to tag the Better Auth team members, Beka and Max (@bekacru and @ping__), to ensure they are aware of this issue and can provide further insights or potential fixes. What further steps are you considering to enhance your authentication security? Feel free to ask if you have more questions! _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@dosubot[bot] commented on GitHub (Feb 12, 2026):

Hi @ivan-dalmet! I'm Dosu and I’m helping the better-auth team.

This is a known security issue: returning USER_NOT_FOUND when sign-up is disabled does leak account existence and enables user enumeration attacks. The intended best practice for OTP endpoints is to always return a generic success response, regardless of whether the user exists, to prevent this kind of information leak. This approach is documented and discussed in several issues and discussions (example, example, example).

In Better Auth v1.4.17, there was a regression where sendVerificationOtp with disableSignUp: true sometimes returns USER_NOT_FOUND for non-existent users, which is not the intended privacy-preserving behavior. This was fixed in a later PR, but is not present in v1.4.17 (details).

If you can't upgrade, you can use an after hook or middleware to intercept USER_NOT_FOUND errors and return a generic success response, but be aware this workaround must be implemented carefully to avoid reintroducing enumeration risk (example workaround).

Best practice is to upgrade to a version where this is fixed, so the endpoint always returns a generic success response for non-existent users when sign-up is disabled.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3891071714 --> @dosubot[bot] commented on GitHub (Feb 12, 2026): <!-- Greeting --> Hi @ivan-dalmet! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known security issue: returning USER_NOT_FOUND when sign-up is disabled does leak account existence and enables user enumeration attacks. The intended best practice for OTP endpoints is to always return a generic success response, regardless of whether the user exists, to prevent this kind of information leak. This approach is documented and discussed in several issues and discussions ([example](https://github.com/better-auth/better-auth/issues/5017), [example](https://github.com/better-auth/better-auth/issues/2803), [example](https://github.com/better-auth/better-auth/issues/7348)). In Better Auth v1.4.17, there was a regression where sendVerificationOtp with disableSignUp: true sometimes returns USER_NOT_FOUND for non-existent users, which is not the intended privacy-preserving behavior. This was fixed in a later PR, but is not present in v1.4.17 ([details](https://github.com/better-auth/better-auth/issues/7696)). If you can't upgrade, you can use an after hook or middleware to intercept USER_NOT_FOUND errors and return a generic success response, but be aware this workaround must be implemented carefully to avoid reintroducing enumeration risk ([example workaround](https://github.com/better-auth/better-auth/discussions/3972)). Best practice is to upgrade to a version where this is fixed, so the endpoint always returns a generic success response for non-existent users when sign-up is disabled. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/41fe3942-535b-4ab5-a0f6-b674dd4e632f?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/41fe3942-535b-4ab5-a0f6-b674dd4e632f?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/41fe3942-535b-4ab5-a0f6-b674dd4e632f?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/41fe3942-535b-4ab5-a0f6-b674dd4e632f?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/41fe3942-535b-4ab5-a0f6-b674dd4e632f?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/41fe3942-535b-4ab5-a0f6-b674dd4e632f?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/41fe3942-535b-4ab5-a0f6-b674dd4e632f?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20known%20security%20issue%3A%20returning%20USER_NOT_FOUND%20when%20sign-up%20is%20disabled%20does%20leak%20account%20existence%20and%20enables%20user%20enumeration%20attacks.%20The%20intended%20best%20practice%20for%20OTP%20endpoints%20is%20to%20always%20return%20a%20generic%20success%20response%2C%20regardless%20of%20whether%20the%20user%20exists%2C%20to%20prevent%20this%20kind%20of%20information%20leak.%20This%20approach%20is%20documented%20and%20discussed%20in%20several%20issues%20and%20discussions%20%28%5Bexample%5D%28https%3A//github.com/better-auth/better-auth/issues/5017%29%2C%20%5Bexample%5D%28https%3A//github.com/better-auth/better-auth/issues/2803%29%2C%20%5Bexample%5D%28https%3A//github.com/better-auth/better-auth/issues/7348%29%29.%0A%0AIn%20Better%20Auth%20v1.4.17%2C%20there%20was%20a%20regression%20where%20sendVerificationOtp%20with%20disableSignUp%3A%20true%20sometimes%20returns%20USER_NOT_FOUND%20for%20non-existent%20users%2C%20which%20is%20not%20the%20intended%20privacy-preserving%20behavior.%20This%20was%20fixed%20in%20a%20later%20PR%2C%20but%20is%20not%20present%20in%20v1.4.17%20%28%5Bdetails%5D%28https%3A//github.com/better-auth/better-auth/issues/7696%29%29.%20%0A%0AIf%20you%20can%27t%20upgrade%2C%20you%20can%20use%20an%20after%20hook%20or%20middleware%20to%20intercept%20USER_NOT_FOUND%20errors%20and%20return%20a%20generic%20success%20response%2C%20but%20be%20aware%20this%20workaround%20must%20be%20implemented%20carefully%20to%20avoid%20reintroducing%20enumeration%20risk%20%28%5Bexample%20workaround%5D%28https%3A//github.com/better-auth/better-auth/discussions/3972%29%29.%20%0A%0ABest%20practice%20is%20to%20upgrade%20to%20a%20version%20where%20this%20is%20fixed%2C%20so%20the%20endpoint%20always%20returns%20a%20generic%20success%20response%20for%20non-existent%20users%20when%20sign-up%20is%20disabled.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/7944)
Author
Owner

@ivan-dalmet commented on GitHub (Feb 12, 2026):

Just to let you know @dosu that 1.5.0-beta.13 seems to have the same issue

<!-- gh-comment-id:3891104798 --> @ivan-dalmet commented on GitHub (Feb 12, 2026): Just to let you know @dosu that `1.5.0-beta.13` seems to have the same issue
Author
Owner

@dosubot[bot] commented on GitHub (Feb 12, 2026):

You're right—this user enumeration issue is still present in 1.5.0-beta.13, but only in certain endpoints. Specifically, the leak happens in the signInEmailOTP endpoint: if the user doesn't exist and disableSignUp is true, it throws a USER_NOT_FOUND error, which reveals account existence. The sendVerificationOTP endpoint, on the other hand, returns a generic { success: true } for non-existent users and does not leak this info (source).

There's no official patch for this in the current beta, but you can work around it by adding an after hook or middleware to intercept USER_NOT_FOUND errors on the affected endpoint and always return a generic success response. If you want an example of how to do this, let me know!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3891123133 --> @dosubot[bot] commented on GitHub (Feb 12, 2026): <!-- Answer --> You're right—this user enumeration issue is still present in 1.5.0-beta.13, but only in certain endpoints. Specifically, the leak happens in the `signInEmailOTP` endpoint: if the user doesn't exist and `disableSignUp` is true, it throws a `USER_NOT_FOUND` error, which reveals account existence. The `sendVerificationOTP` endpoint, on the other hand, returns a generic `{ success: true }` for non-existent users and does not leak this info ([source](https://github.com/better-auth/better-auth/blob/0669ed804d8e0ada004c5ba266a6a12be6557cfc/packages/better-auth/src/plugins/email-otp/routes.ts#L121-L709)). There's no official patch for this in the current beta, but you can work around it by adding an after hook or middleware to intercept `USER_NOT_FOUND` errors on the affected endpoint and always return a generic success response. If you want an example of how to do this, let me know! <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/dce972f2-32e6-4e8d-9a2a-64ff7a72445d?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/dce972f2-32e6-4e8d-9a2a-64ff7a72445d?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/dce972f2-32e6-4e8d-9a2a-64ff7a72445d?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/dce972f2-32e6-4e8d-9a2a-64ff7a72445d?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/dce972f2-32e6-4e8d-9a2a-64ff7a72445d?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/dce972f2-32e6-4e8d-9a2a-64ff7a72445d?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/dce972f2-32e6-4e8d-9a2a-64ff7a72445d?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=You%27re%20right%E2%80%94this%20user%20enumeration%20issue%20is%20still%20present%20in%201.5.0-beta.13%2C%20but%20only%20in%20certain%20endpoints.%20Specifically%2C%20the%20leak%20happens%20in%20the%20%60signInEmailOTP%60%20endpoint%3A%20if%20the%20user%20doesn%27t%20exist%20and%20%60disableSignUp%60%20is%20true%2C%20it%20throws%20a%20%60USER_NOT_FOUND%60%20error%2C%20which%20reveals%20account%20existence.%20The%20%60sendVerificationOTP%60%20endpoint%2C%20on%20the%20other%20hand%2C%20returns%20a%20generic%20%60%7B%20success%3A%20true%20%7D%60%20for%20non-existent%20users%20and%20does%20not%20leak%20this%20info%C2%A0%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/blob/0669ed804d8e0ada004c5ba266a6a12be6557cfc/packages/better-auth/src/plugins/email-otp/routes.ts%23L121-L709%29%29.%0A%0AThere%27s%20no%20official%20patch%20for%20this%20in%20the%20current%20beta%2C%20but%20you%20can%20work%20around%20it%20by%20adding%20an%20after%20hook%20or%20middleware%20to%20intercept%20%60USER_NOT_FOUND%60%20errors%20on%20the%20affected%20endpoint%20and%20always%20return%20a%20generic%20success%20response.%20If%20you%20want%20an%20example%20of%20how%20to%20do%20this%2C%20let%20me%20know%21)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/7944)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19581