fix(P1): rate-limit auth endpoints — F7

Pre-audit: 20 rapid wrong-password attempts all returned 401 with no
lockout. Brute-force open.

Post-fix: better-auth's built-in rate limiter caps /sign-in/email at
5 attempts per 60s. Verified live — attempts 1-5 return 401, attempt 6+
returns 429 "Too many requests".

Same tight cap applied to /sign-up/email, /forget-password,
/reset-password. Default 120/min for everything else so legitimate
multi-widget dashboards aren't hampered.

Memory storage in this commit (resets on restart). Production multi-replica
swap to `storage: 'database'` planned for a follow-up once the
rateLimit migration is run.

Also: in production, trust X-Forwarded-For / X-Real-IP so the IP that
rate-limit + audit logging see is the real client, not the proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 22:41:47 +02:00
parent 2c57082d8d
commit 27f8db4c67

View File

@@ -109,6 +109,31 @@ function buildAuth() {
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict' as const,
},
// Trust the X-Forwarded-For chain when running behind Caddy/Nginx in
// production so the IP that rate-limit + audit logging see is the
// real client, not the proxy. Skipped in dev (no proxy in front).
ipAddress: {
ipAddressHeaders: isProd ? ['x-forwarded-for', 'x-real-ip'] : [],
},
},
// Rate limiting (post-audit F7) — without this, brute-force is wide
// open. Tight caps on the credential-eating endpoints; loose default
// for everything else so legitimate fan-out (multi-widget dashboards
// that hit /get-session repeatedly) isn't hampered.
rateLimit: {
enabled: true,
window: 60,
max: 120,
// Memory storage resets on restart. For multi-replica prod, swap
// to `storage: 'database'` once the rateLimit migration is run.
storage: 'memory',
customRules: {
'/sign-in/email': { window: 60, max: 5 },
'/sign-up/email': { window: 60, max: 3 },
'/forget-password': { window: 60, max: 3 },
'/reset-password': { window: 60, max: 5 },
},
},
logger: {