Authentication endpoints without rate limiting are open to brute-force and credential stuffing attacks at internet speed. CWE-799 (Improper Control of Interaction Frequency) and OWASP A07 (Identification & Authentication Failures) cover this class of vulnerability. NIST 800-53 AC-7 requires automated lockout mechanisms on authentication functions. Password-reset endpoints are particularly dangerous: an unrate-limited /api/forgot-password lets an attacker spam reset emails to exhaust users and potentially enumerate valid accounts. Unlike account-level lockout, IP-level rate limiting also stops automated registration abuse and prevents enumeration via timing attacks.
High because unprotected auth endpoints allow automated credential attacks at machine speed, with no throttle on how quickly an attacker can test millions of password candidates.
Apply rate limits to every auth endpoint — login, register, forgot-password, reset-password, and MFA verify. Using Upstash for edge-compatible sliding-window limits:
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const loginLimiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '15 m'),
})
export async function POST(req: Request) {
const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1'
const { success, reset } = await loginLimiter.limit(ip)
if (!success) {
return Response.json({ error: 'Too many attempts' }, {
status: 429,
headers: { 'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)) },
})
}
}
Protect /api/auth/forgot-password with a tighter limit (3 per 15 minutes) than login (10 per minute).
ID: security-hardening.auth-session.auth-rate-limiting
Severity: high
What to look for: Enumerate every authentication endpoint (login, register, password reset, token refresh). For each, check /api/auth/*, /api/login, /api/register, /api/forgot-password, /api/reset-password, and /api/verify-email endpoints (and any custom equivalents) for rate limiting. Look for @upstash/ratelimit, express-rate-limit, rate-limiter-flexible, or equivalent middleware. Verify limits are per IP or per identifier (not just global).
Pass criteria: Login, registration, password reset, and 2FA verification endpoints all have rate limits configured. Limits are restrictive enough to meaningfully slow brute force — no more than 10 requests per minute per IP on login, and no more than 5 requests per minute on password reset. Report: "X auth endpoints found, all Y have rate limiting configured."
Fail criteria: Auth endpoints have no rate limiting. Or only one of the auth endpoints is protected while others are left open.
Skip (N/A) when: All auth is handled by a managed provider (Clerk, Auth0, Supabase Auth) with built-in rate limiting that you have confirmed is enabled.
Detail on fail: Name the unprotected endpoints. Example: "POST /api/auth/login has no rate limiting — unlimited credential stuffing attempts possible" or "Rate limiting applied to login but not to /api/auth/forgot-password — password reset endpoint spammable"
Remediation: Apply rate limiting to all auth endpoints. Using Upstash for edge-compatible rate limiting:
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
export const authLimiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '15 m'), // 5 per 15 minutes
analytics: true,
})
// In your login route handler:
export async function POST(req: Request) {
const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1'
const { success, reset } = await authLimiter.limit(ip)
if (!success) {
return Response.json(
{ error: 'Too many attempts. Try again later.' },
{
status: 429,
headers: { 'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)) },
}
)
}
// ... login logic
}