Login / signup / password-reset endpoints have rate limits
Why it matters
Authentication endpoints without rate limits are a credential-stuffing accelerant: attackers replay leaked username/password pairs from prior breaches against the /login endpoint at ten thousand requests per second from a distributed botnet, and any account whose user reuses a password is compromised. 23andMe's October 2023 breach — 6.9 million users exposed, followed by an FTC settlement and class-action payouts — was a pure credential-stuffing attack that succeeded specifically because login attempts were not rate-limited. Signup endpoints are equally risky: unrate-limited signup enables automated account-creation to harvest free trials, spam the platform, or warm up accounts for later abuse. Password-reset endpoints without rate limits enable enumeration (probing whether an email exists by reading response timing) and SMS/email-cost pumping. AI coding tools omit rate limits on auth endpoints almost universally, because the happy-path flow works fine with a single request.
Severity rationale
High because credential-stuffing at scale produces real account takeovers and direct regulatory liability (FTC, state AG actions, GDPR breach reporting), and the remediation is cheap while the exposure compounds over time as breach corpora grow.
Remediation
Use @upstash/ratelimit with Vercel KV (or Redis) to gate login, signup, and password-reset by IP + email:
import { Ratelimit } from '@upstash/ratelimit';
import { kv } from '@vercel/kv';
const limiter = new Ratelimit({ redis: kv, limiter: Ratelimit.slidingWindow(5, '1 m') });
const { success } = await limiter.limit(`login:${ip}:${email}`);
if (!success) return new Response('Too many attempts', { status: 429 });
For Supabase Auth, enable the built-in rate limits in supabase/config.toml under [auth.rate_limit]. Run api-security for deeper coverage of enumeration-resistance, CAPTCHA escalation, and MFA.
Detection
- ID:
rate-limit-on-auth-endpoints - Severity:
high - What to look for: Enumerate handlers in the three required categories. login:
/api/auth/login|signin,/api/login|signin, NextAuthpages/api/auth/[...nextauth]POST, SupabasesignInWithPassword|signInWithOAuth|signInWithOtpcall sites, ClerksignIn(), plus'use server'actions namedlogin|signIn|authenticate|loginAction. signup:/api/auth/register|signup,/api/register|signup, SupabasesignUp, ClerksignUp, plus equivalent server actions. password-reset:/api/auth/reset-password|forgot-password|verify-password,/api/reset-password, SupabaseresetPasswordForEmail/updateUser({ password }), ClerkcreatePasswordResetFlow, plus server actions. For each, verify one of: (a) aratelimit.limit(...)/@upstash/ratelimit/rate-limiter-flexible/express-rate-limit/ project-localrateLimit(key, limit, window)call before the auth attempt; (b) Supabase built-in limits:supabase/config.tomlunder[auth.rate_limit]with non-trivial values; (c) Vercel Firewall rules invercel.jsonunderfirewall.rulestargeting the auth paths. - Pass criteria: ALL THREE of login AND signup AND password-reset are rate-limited before the auth attempt. Key combines IP + identifier (email/username). Threshold is auth-appropriate (~5/min, not 100/min).
- Fail criteria: Any of the three runs its auth call without a limiter. Partial coverage fails — login gated but signup/reset open. A limiter imported but never called fails. A generic
/api/*middleware at 100/min does NOT count unless you trace its matcher and confirm it covers all three paths AND its threshold is auth-appropriate.// TODO: add rate limitdoes not count. - Skip (N/A) when: No auth system. Quote:
"package.json lacks @supabase/*, next-auth, @clerk/*, @auth0/*; no /api/auth/* handlers; no server actions named login / signup / resetPassword". - Report even on pass: Name mechanism + threshold per category:
"login (app/api/auth/login/route.ts): @upstash/ratelimit 5/min per IP+email. signup (app/api/auth/register/route.ts): same limiter. password-reset (app/actions/auth.ts resetPasswordAction): same limiter. All three gated." - Detail on fail:
"signup and password-reset lack rate limits. login uses @upstash/ratelimit 5/min; app/api/auth/register/route.ts:8 calls supabase.auth.signUp() with no preceding limiter; app/actions/auth.ts:14 resetPasswordAction calls resetPasswordForEmail with no limiter. All three required." - Cross-reference: For enumeration-resistance, CAPTCHA escalation, IP reputation, and MFA enforcement, run
api-security. - Remediation:
Apply the same pattern to signup AND password-reset — all three need gating. For Supabase Auth, also configure// app/api/login/route.ts import { Ratelimit } from '@upstash/ratelimit'; import { kv } from '@vercel/kv'; const limiter = new Ratelimit({ redis: kv, limiter: Ratelimit.slidingWindow(5, '1 m') }); export async function POST(req: Request) { const { email } = await req.json(); const ip = req.headers.get('x-forwarded-for') ?? 'unknown'; const { success } = await limiter.limit(`login:${ip}:${email}`); if (!success) return new Response('Too many attempts', { status: 429 }); // ...attempt login }[auth.rate_limit]insupabase/config.toml.
Taxons
History
- 2026-04-22·v1.0.0·Initial authoring via Phase 9 consequence-first restructure·by editorial
- 2026-04-23·v1.1.0·Phase 9.1 tighten — all three of login + signup + password-reset must be rate-limited (partial coverage = fail); explicit endpoint paths enumerated including server-action equivalents.·by phase-9-1-stack-scan-v3-1
- 2026-04-25·v1.1.1·v3.1.0 pre-ship trim — prose compression for under-80K MCP cap; merged overlapping Fail-criteria / Do-NOT-pass-when sections; compressed enumeration prose; one remediation example per pattern. No semantic change; anti-sycophancy guards preserved.·by phase-9-1-stack-scan-v3-1