Skip to content

Cap sign-in code guesses and code emails in the database - #434

Merged
aamoghS merged 2 commits into
devfrom
fix/signin-code-attempts
Sep 25, 2026
Merged

aamoghS merged 2 commits into
devfrom
fix/signin-code-attempts

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Bugs (security)

  • Brute force: a wrong code never burned the code, and the only brake was the in-memory limiter — one per instance. With App Hosting's up-to-10 instances a live 6-digit code took ~10× the guesses it was sized for (~5%/day).
  • Email flooding: requesting a code had no limit. Anyone could flood any inbox and burn the shared ~500/day Gmail sending quota, blocking every member's sign-in and acceptance mail.

Fix

  • verificationToken.attempts (additive column, default 0). Each wrong guess charges the live code; it's deleted after 5 misses — enforced in the DB, so it holds across instances.
  • A code request within 1 minute of the last one sends nothing and keeps the code already in the inbox (the live code's expiry says when it was issued). The login page still moves on to /verify, so it's invisible to a real user.

Schema

verificationToken.attempts integer not null default 0 — additive; the deploy's drizzle-kit push applies it.

Tests

New route test: a wrong guess increments attempts and runs the burn-at-limit delete (fails on the old route, passes on this one). Typecheck 9/9, lint 8/8, 829 tests.

Not fixed

Per-address throttling doesn't stop one attacker spreading requests over many addresses to exhaust the quota; that needs a global send cap or a CAPTCHA on sign-in.


Note

High Risk
Changes email sign-in issuance and verification in security-critical paths; requires the additive attempts column to be applied before deploy, and advisory-lock/transaction behavior must stay correct under concurrency.

Overview
Hardens email sign-in codes with database-backed limits that work across app instances.

Brute-force protection: verificationToken gains an attempts counter (default 0). On a wrong code in verify-email, the live custom:% row is incremented and deleted after 5 failed guesses, so guessing limits are shared cluster-wide instead of per-instance in-memory rate limits.

Email abuse protection: Code issuance in packages/auth now allows at most one email per address per minute. A transaction with pg_advisory_xact_lock(hashtext(identifier)) checks for a recently issued code, skips send if one exists, otherwise replaces tokens and inserts the new code. Failed SMTP sends remove the new token so a retry within the minute is not blocked by a “phantom” recent code.

Adds a Vitest for wrong-guess behavior (increment + burn delete).

Reviewed by Cursor Bugbot for commit b90eed7. Bugbot is set up for automated code reviews on this repo. Configure here.

- A wrong code never burned the code, and the only brake was the
  in-memory rate limiter, one per instance: with App Hosting's ten
  instances a live 6-digit code took ten times the guesses it was sized
  for (~5% a day). verificationToken gains an additive attempts column;
  each miss charges the live code, and it is deleted after 5, however
  requests spread across instances.
- Requesting a code had no limit at all, so anyone could flood any
  inbox and burn the shared sending quota. A request within a minute of
  the last code now sends nothing and keeps the code already in the
  inbox (the live code's expiry says when it was issued), across
  instances.

Schema: verificationToken.attempts integer not null default 0
(additive; applied by the deploy's drizzle-kit push).
@github-actions
github-actions Bot requested a review from aamoghS as a code owner September 25, 2026 03:55
@aamoghS aamoghS changed the title Feature: fix/signin-code-attempts to dev Cap sign-in code guesses and code emails in the database Sep 25, 2026

@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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3736434. Configure here.

Comment thread packages/auth/src/config.ts Outdated
LIMIT 1
`);
if (recent.rows.length > 0) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Failed send blocks email retry

Medium Severity

The code row is written before SMTP. If send fails, that row still looks newly issued, so the next request returns without sending or throwing. NextAuth then reports success and the user is sent to /verify with no email in their inbox.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3736434. Configure here.

Comment thread packages/auth/src/config.ts Outdated
LIMIT 1
`);
if (recent.rows.length > 0) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Concurrent sends bypass email cap

Medium Severity

The recent-token check, delete, and insert are separate statements with no transaction or row lock. Parallel sign-in requests all see no live code and each send a message, so hammering across instances still floods the inbox.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3736434. Configure here.

…ails

Review follow-ups:
- The recent-code check, delete and insert were separate statements, so
  parallel sign-ins across instances all saw no live code and each sent
  one. They now run in one transaction under
  pg_advisory_xact_lock(hashtext(identifier)).
- The code row is written before SMTP. If the send failed, the row still
  looked freshly issued, so a retry within the minute sent nothing and
  reported success, leaving the user at /verify with no email. A failed
  send now deletes that code before throwing.
@github-actions github-actions Bot added the bug Something isn't working label Sep 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor Author

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Visit the preview URL for this PR (updated for commit b90eed7):

https://hacklytics2027--pr-434-zxgnukfh.web.app

(expires Fri, 02 Oct 2026 13:20:11 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: c48ba34db61581e25fe2978355160b5eefe0e83f

@aamoghS
aamoghS merged commit 91d30a3 into dev Sep 25, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⤵️ pull bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant