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.
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
## 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)
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:resetPasswordrate-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:truepackages/api/src/middleware/rate-limit.ts:218 — Redis-flap degrades to maxRequests/10, self-locks legit userspackages/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-forviatrustProxyconfig). 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
allowed: false(not true)docs/security-architecture.mdwith key-schema tableParent Epic: #1
Source: Full-Codebase Security Audit 2026-04-16 (A-3, A-4, A-9, A-18)
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.