Security [CRITICAL]: Rate-limiter only keys by email — IP-based brute-force and targeted lockout possible #37

Closed
opened 2026-04-16 22:05:08 +02:00 by Hartmut · 1 comment
Owner

Problem

Login rate-limit (authRateLimiter(email)) keys only on email. An attacker can (a) enumerate/brute-force arbitrary emails from one IP with no global limit, and (b) lock out any known user by submitting 5 wrong passwords in 15 min (account-lockout DoS). Additional: resetPassword rate-limit keys on the reset-token (always new) → effectively no limit.

Evidence

  • apps/web/src/server/auth.ts:55 — await authRateLimiter(email.toLowerCase())
  • packages/api/src/router/auth.ts:81 — rl = await authRateLimiter(input.token)
  • packages/api/src/middleware/rate-limit.ts:223 — empty key returns allowed:true
  • packages/api/src/middleware/rate-limit.ts:218 — Redis-flap degrades to maxRequests/10, self-locks legit users
  • packages/api/src/router/user.ts:245 — TOTP-verify rate-limit keyed on userId only (lockout DoS)

Impact

(a) Credential-stuffing attacks across emails unbounded. (b) Targeted account-lockout DoS against any known user. (c) Reset-password endpoint lacks effective limit → DB load DoS. (d) TOTP endpoint allows per-user lockout.

Proposed Fix

Add second rate-limit bucket keyed on request IP (x-forwarded-for via trustProxy config). Key pattern: authRateLimiter(['email:' + email, 'ip:' + ip]). Fail closed on empty key. For reset-password: key on resolved email AFTER token lookup, OR on IP with higher limit. For TOTP: add IP-second-key.

Acceptance Criteria

  • Login/reset/TOTP all rate-limited by IP in addition to user-key
  • Empty/whitespace key returns allowed: false (not true)
  • Redis-flap fallback reduces limit by ≤ 50%, not 90%
  • Documented in docs/security-architecture.md with key-schema table

Parent Epic: #1
Source: Full-Codebase Security Audit 2026-04-16 (A-3, A-4, A-9, A-18)

## Problem Login rate-limit (`authRateLimiter(email)`) keys only on email. An attacker can (a) enumerate/brute-force arbitrary emails from one IP with no global limit, and (b) lock out any known user by submitting 5 wrong passwords in 15 min (account-lockout DoS). Additional: `resetPassword` rate-limit keys on the reset-token (always new) → effectively no limit. ## Evidence - `apps/web/src/server/auth.ts:55 — await authRateLimiter(email.toLowerCase())` - `packages/api/src/router/auth.ts:81 — rl = await authRateLimiter(input.token)` - `packages/api/src/middleware/rate-limit.ts:223 — empty key returns allowed:true` - `packages/api/src/middleware/rate-limit.ts:218 — Redis-flap degrades to maxRequests/10, self-locks legit users` - `packages/api/src/router/user.ts:245 — TOTP-verify rate-limit keyed on userId only (lockout DoS)` ## Impact (a) Credential-stuffing attacks across emails unbounded. (b) Targeted account-lockout DoS against any known user. (c) Reset-password endpoint lacks effective limit → DB load DoS. (d) TOTP endpoint allows per-user lockout. ## Proposed Fix Add second rate-limit bucket keyed on request IP (`x-forwarded-for` via `trustProxy` config). Key pattern: `authRateLimiter(['email:' + email, 'ip:' + ip])`. Fail closed on empty key. For reset-password: key on resolved email AFTER token lookup, OR on IP with higher limit. For TOTP: add IP-second-key. ## Acceptance Criteria - [ ] Login/reset/TOTP all rate-limited by IP in addition to user-key - [ ] Empty/whitespace key returns `allowed: false` (not true) - [ ] Redis-flap fallback reduces limit by ≤ 50%, not 90% - [ ] Documented in `docs/security-architecture.md` with key-schema table --- Parent Epic: #1 Source: Full-Codebase Security Audit 2026-04-16 (A-3, A-4, A-9, A-18)
Hartmut added the security label 2026-04-16 22:05:08 +02:00
Author
Owner

Resolved in main commit 3c5d1d3 (security: rate-limit IP-keyed, fail-closed on empty key). Rate-limit now keys on IP + email; empty-key paths fail closed.

Resolved in main commit 3c5d1d3 (`security: rate-limit IP-keyed, fail-closed on empty key`). Rate-limit now keys on IP + email; empty-key paths fail closed.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Hartmut/CapaKraken#37