From 27f8db4c671e61dbbd4b4d734561cd17560b625a Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 14 May 2026 22:41:47 +0200 Subject: [PATCH] =?UTF-8?q?fix(P1):=20rate-limit=20auth=20endpoints=20?= =?UTF-8?q?=E2=80=94=20F7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/lib/auth/index.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index fb30c773..9fd77827 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -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: {