Skip to content

fix(auth): rate limit the password reset endpoints - #6553

Merged
waleedlatif1 merged 1 commit into
stagingfrom
fix/forget-password-rate-limit
Aug 11, 2026
Merged

fix(auth): rate limit the password reset endpoints#6553
waleedlatif1 merged 1 commit into
stagingfrom
fix/forget-password-rate-limit

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • /api/auth/forget-password calls auth.api.requestPasswordReset without headers, which bypasses Better Auth's rate limiter entirely (it lives in the HTTP router, not the endpoint). The route was an unthrottled email-send amplifier keyed on any address a caller chose
  • Adds a per-IP budget before parsing (cheap pre-parse gate) and a per-recipient budget — no per-IP limit can stop a distributed attempt to bomb one mailbox
  • The recipient key is normalized and hashed, so the bucket store never holds an address and long inputs cannot inflate key cardinality
  • Enforced before any user lookup and identically whether or not the account exists, so a 429 is not an account-existence oracle
  • Also throttles /api/auth/reset-password, which had no rate limiting and is an online token-guessing surface

Why not just pass headers to auth.api.*

That looks like the deeper fix but regresses behavior: Better Auth's limiter throws an APIError, which these routes' catch blocks project as a 500 with the raw message, not a 429. It is also per-IP only, so it cannot express the per-recipient dimension that mailbox bombing actually requires.

internalRateLimits also cannot cover these routes — its enforce runs after authenticate and takes a Principal, and defineInternalJsonRoute has no unauthenticated path. Imperative enforce* is the established pattern here (19 route files already use it; zero use a declarative throttling policy, because none exists).

Type of Change

  • Bug fix (security hardening)

Testing

bun run type-check clean; 230 tests across 16 files in app/api/auth + lib/core/rate-limiter pass; bun run lint and check:api-validation clean. Both new rate-limit tests verified to fail when their guard is stubbed out.

Follow-up (not in this PR)

rate_limit_bucket has no TTL or cleanup job on the Postgres adapter, so the recipient dimension is attacker-controlled key space that grows unbounded on Postgres-backed deployments. Redis-backed deployments are unaffected (redis-token-bucket.ts:41 sets EXPIRE).

app/api/contact, app/api/demo-requests, and app/api/help/integration-request still hand-roll what route-helpers does, including a byte-identical copy of DEFAULT_PUBLIC_IP_ROUTE_LIMIT. Unifying them changes the user-visible 429 string on three public forms, so it is left out of this change.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

`/api/auth/forget-password` calls `auth.api.requestPasswordReset` without
headers, which bypasses Better Auth's rate limiter entirely — that limiter
lives in the HTTP router, not the endpoint. The route was an unthrottled
email-send amplifier keyed on any address a caller chose.

Add two dimensions via the existing route-helper family: a per-IP budget
before parsing (cheap pre-parse gate), and a per-recipient budget, since no
per-IP limit can stop a distributed attempt to bomb one mailbox. The recipient
key is normalized and hashed, so the bucket store never holds an address and
long inputs cannot inflate key cardinality. It is enforced before any user
lookup and identically whether or not the account exists, so a 429 is not an
account-existence oracle.

Also throttle `/api/auth/reset-password`, which had none and is an online
token-guessing surface.

Passing headers to `auth.api.*` is deliberately not the fix: Better Auth's
limiter throws an APIError that these routes' catch blocks project as a 500,
and it cannot express the per-recipient dimension.
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 11, 2026 7:07pm

Request Review

@cursor

cursor Bot commented Aug 11, 2026

Copy link
Copy Markdown

PR Summary

Cursor Bugbot is generating a summary for commit d2df8b3. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds rate limiting to the unauthenticated password-reset flows to limit email amplification and reset-token guessing.

  • Applies an early per-IP budget to both password-reset endpoints.
  • Adds a normalized, hashed per-recipient budget to password-reset email requests.
  • Adds route-level tests for throttling, short-circuiting, recipient normalization, and hashed keys.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/app/api/auth/forget-password/route.ts Adds early per-IP throttling and a normalized, hashed per-recipient budget before requesting a reset email.
apps/sim/app/api/auth/reset-password/route.ts Adds an early endpoint-specific per-IP budget before parsing or consuming a reset token.
apps/sim/lib/core/rate-limiter/route-helpers.ts Introduces a reusable recipient limiter that stores only a normalized email hash in the bucket key.
apps/sim/app/api/auth/forget-password/route.test.ts Tests recipient exhaustion, normalization, hashed keys, and IP-limit short-circuiting.
apps/sim/app/api/auth/reset-password/route.test.ts Tests that exhausted IP budgets return 429 without invoking password reset.
apps/sim/lib/core/rate-limiter/index.ts Exports the new recipient rate-limit helper through the package entry point.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Forget as Forget-password route
  participant Limiter as Rate limiter
  participant Auth as Better Auth
  Client->>Forget: POST email
  Forget->>Limiter: Check per-IP budget
  alt IP budget exhausted
    Limiter-->>Client: 429 + Retry-After
  else IP budget available
    Forget->>Forget: Parse and validate request
    Forget->>Limiter: Check hashed recipient budget
    alt Recipient budget exhausted
      Limiter-->>Client: 429 + Retry-After
    else Recipient budget available
      Forget->>Auth: Request password-reset email
      Auth-->>Client: Success response
    end
  end
Loading

Reviews (2): Last reviewed commit: "fix(auth): rate limit the password reset..." | Re-trigger Greptile

Comment thread apps/sim/app/api/auth/forget-password/route.ts
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit d2df8b3. Configure here.

@waleedlatif1
waleedlatif1 merged commit 31bfcdd into staging Aug 11, 2026
29 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/forget-password-rate-limit branch August 11, 2026 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant