Cap sign-in code guesses and code emails in the database - #434
Conversation
- 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).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ 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.
| LIMIT 1 | ||
| `); | ||
| if (recent.rows.length > 0) return; | ||
|
|
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 3736434. Configure here.
| LIMIT 1 | ||
| `); | ||
| if (recent.rows.length > 0) return; | ||
|
|
There was a problem hiding this comment.
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.
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.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
|
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 |


Bugs (security)
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./verify, so it's invisible to a real user.Schema
verificationToken.attempts integer not null default 0— additive; the deploy'sdrizzle-kit pushapplies it.Tests
New route test: a wrong guess increments
attemptsand 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
attemptscolumn 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:
verificationTokengains anattemptscounter (default 0). On a wrong code inverify-email, the livecustom:%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/authnow allows at most one email per address per minute. A transaction withpg_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.