Big security issue: problematic execution order and non-functioning rate limiter (easy DoS attack) #510

Closed
opened 2026-03-13 07:50:25 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @JosipPardon on GitHub (Jan 2, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Setup email OTP sign in as described in docs.

Add intentional error throwing to sendVerificationOTP:

async sendVerificationOTP({ email, otp, type }) {
  throw new Error("Failed to send email");
}

Build your app and start it in production mode.

Then run DoS attack script:

setInterval(() => {
  for (let i = 0; i < 100; i++) {
    const result = fetch(
      "http://localhost:3000/api/auth/email-otp/send-verification-otp",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          email: "dummy@mail.com", //but attacker would use random valid-looking emails
          type: "sign-in",
        }),
      }
    );
  }
  console.log("Sent 100 requests");
}, 7500);

Current vs. Expected behavior

There are two major problems.

Despite sending OTPs will be unsuccessful, they will be stored in the database. These OTPs will never be used and deleted; they just take up space.

OTP is stored before sendVerificationOTP is successfully executed, which is problematic. Storing should not happen if sendVerificationOTP fails and throws an error.

The second problem is associated with rate limiters. Docs say that by default the limit is 100 requests per 60 seconds.

If you let the above DoS script run for around 60 seconds, you will end up with around new 600 entries in the verification table. This should not be possible if the limiter is configured as described above. So, yeah, the default rate limiter does not work. I have not tried setting custom limits, maybe it would also not work, but dysfunctional default limiter is problematic enough.

All this means an attacker can fill the database with around 1,000,000 zombie rows in a single day. And if he attacks from multiple devices, this number is much bigger.


Additionally to all this, installation part of docs should instruct developer to set up cron job which cleans tables from junk rows. This is not mentioned anywhere, but it is pretty important. Also, it would be nice if function dedicated to this was available, like cleanBetterAuthTables.

What version of Better Auth are you using?

1.1.8

Provide environment information

MacOS, Next.js 15, Safari

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

Backend, Package

Auth config (if applicable)

No response

Additional context

No response

Originally created by @JosipPardon on GitHub (Jan 2, 2025). ### Is this suited for github? - [X] Yes, this is suited for github ### To Reproduce Setup email OTP sign in as described in [docs](https://www.better-auth.com/docs/plugins/email-otp). Add intentional error throwing to `sendVerificationOTP`: ```ts async sendVerificationOTP({ email, otp, type }) { throw new Error("Failed to send email"); } ``` Build your app and start it in production mode. Then run DoS attack script: ```ts setInterval(() => { for (let i = 0; i < 100; i++) { const result = fetch( "http://localhost:3000/api/auth/email-otp/send-verification-otp", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ email: "dummy@mail.com", //but attacker would use random valid-looking emails type: "sign-in", }), } ); } console.log("Sent 100 requests"); }, 7500); ``` ### Current vs. Expected behavior There are two major problems. Despite sending OTPs will be unsuccessful, they will be stored in the database. These OTPs will never be used and deleted; they just take up space. OTP is stored before `sendVerificationOTP` is successfully executed, which is problematic. Storing should not happen if `sendVerificationOTP` fails and throws an error. The second problem is associated with rate limiters. [Docs](https://www.better-auth.com/docs/concepts/rate-limit) say that by default the limit is 100 requests per 60 seconds. If you let the above DoS script run for around 60 seconds, you will end up with around new 600 entries in the verification table. This should not be possible if the limiter is configured as described above. So, yeah, the default rate limiter does not work. I have not tried setting custom limits, maybe it would also not work, but dysfunctional default limiter is problematic enough. All this means an attacker can fill the database with around 1,000,000 zombie rows in a single day. And if he attacks from multiple devices, this number is much bigger. *** Additionally to all this, installation part of docs should instruct developer to set up cron job which cleans tables from junk rows. This is not mentioned anywhere, but it is pretty important. Also, it would be nice if function dedicated to this was available, like `cleanBetterAuthTables`. ### What version of Better Auth are you using? 1.1.8 ### Provide environment information ```bash MacOS, Next.js 15, Safari ``` ### Which area(s) are affected? (Select all that apply) Backend, Package ### Auth config (if applicable) _No response_ ### Additional context _No response_
GiteaMirror added the bug label 2026-03-13 07:50:25 -05:00
Author
Owner

@Bekacru commented on GitHub (Jan 2, 2025):

Hey, thanks for reporting the issue! In the future, if you come across a security issue, please avoid reporting it publicly. Instead, email us at security@better-auth.com or reach out to me on Discord. We typically respond and patch issues within a day. If we don’t respond or release a patch within 3–4 days, feel free to report it publicly.

@Bekacru commented on GitHub (Jan 2, 2025): Hey, thanks for reporting the issue! In the future, if you come across a security issue, please avoid reporting it publicly. Instead, email us at `security@better-auth.com` or reach out to me on Discord. We typically respond and patch issues within a day. If we don’t respond or release a patch within 3–4 days, feel free to report it publicly.
Author
Owner

@JosipPardon commented on GitHub (Jan 2, 2025):

@Bekacru Thanks for responding so fast to issue. I am incredibly impressed by your dedication and hard work you put in this library. I hope I will soon be able to financially support you.

Yeah, you are right. Security issues should not be reported publicly, my mistake.

I checked your changes, it seems that you missed one part of my post: "Storing should not happen if sendVerificationOTP fails and throws an error."

Should I submit new issue with this?

@JosipPardon commented on GitHub (Jan 2, 2025): @Bekacru Thanks for responding so fast to issue. I am incredibly impressed by your dedication and hard work you put in this library. I hope I will soon be able to financially support you. Yeah, you are right. Security issues should not be reported publicly, my mistake. I checked your changes, it seems that you missed one part of my post: "Storing should not happen if sendVerificationOTP fails and throws an error." Should I submit new issue with this?
Author
Owner

@JosipPardon commented on GitHub (Jan 2, 2025):

Also, it is probably good idea to add this to issue template: "If you want to report security issue, contact us at: (...). Submit it publicly only if you don't get response in 3-4 days".

@JosipPardon commented on GitHub (Jan 2, 2025): Also, it is probably good idea to add this to issue template: "If you want to report security issue, contact us at: (...). Submit it publicly only if you don't get response in 3-4 days".
Author
Owner

@Bekacru commented on GitHub (Jan 2, 2025):

I checked your changes, it seems that you missed one part of my post: "Storing should not happen if sendVerificationOTP fails and throws an error."

Should I submit new issue with this?

the problem is error could be thrown for different reasons and you still may have sent the email even when you throw an error. Reverting and deleting the token in those cases is not ideal. Also you can always delete the entry within the callback.

@Bekacru commented on GitHub (Jan 2, 2025): > I checked your changes, it seems that you missed one part of my post: "Storing should not happen if sendVerificationOTP fails and throws an error." > > Should I submit new issue with this? the problem is error could be thrown for different reasons and you still may have sent the email even when you throw an error. Reverting and deleting the token in those cases is not ideal. Also you can always delete the entry within the callback.
Author
Owner

@Bekacru commented on GitHub (Jan 2, 2025):

Also, it is probably good idea to add this to issue template: "If you want to report security issue, contact us at: (...). Submit it publicly only if you don't get response in 3-4 days".

yeah good suggestion!

@Bekacru commented on GitHub (Jan 2, 2025): > Also, it is probably good idea to add this to issue template: "If you want to report security issue, contact us at: (...). Submit it publicly only if you don't get response in 3-4 days". yeah good suggestion!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#510