From 2c281fc50706b23296d53bc677d3d4984d28d34e Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Wed, 13 May 2026 11:53:06 +0000 Subject: [PATCH] Task #525: Auth endpoint rate limiting (MR-H3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the remaining High-severity finding from .local/security/manual-review.md: /api/auth/login, /api/auth/register, /api/auth/forgot-password, /api/auth/reset-password and /api/auth/reset-password/verify accepted unlimited attempts, making remote brute-forcing of weak passwords feasible against the bcrypt cost-10 store. Changes ------- - New artifacts/api-server/src/lib/authRateLimit.ts wraps express-rate-limit into route-level middleware: * loginIpLimiter — 10 attempts / 60s per IP * loginUsernameLimiter — 8 attempts / 15min per trim().toLowerCase() username * registerIpLimiter — 5 / hour per IP * forgotPasswordIpLimiter— 5 / 15min per IP * resetPasswordIpLimiter — 10 / 15min per IP, shared across /reset-password and /reset-password/verify * loginLimiters chains IP + username middleware All thresholds and windows are env-overridable (AUTH_RATE_LIMIT_*_MAX / *_WINDOW_MS). Throttled responses are JSON: 429 { error: "too_many_requests", message: ... }. In non-production, requests originating from loopback (127.0.0.1, ::1, ::ffff:127.*) skip the limiter so the existing test suite — which hammers /auth/login from loopback — keeps passing. Production never skips. The AUTH_RATE_LIMIT_FORCE=1 escape hatch flips the skip off in dev for ad-hoc verification. - routes/auth.ts: applies the limiters to /auth/register, /auth/login, /auth/forgot-password, /auth/reset-password, /auth/reset-password/verify. trust proxy was already set to 1 in app.ts so X-Forwarded-For from the Replit edge drives req.ip in production. - New artifacts/api-server/tests/auth-rate-limit.test.mjs (6 tests). Each test routes through a unique X-Forwarded-For 10.x.x.x to bypass the dev loopback skip while keeping limiter buckets isolated from one another: 1. login per-IP: 10 wrong-cred attempts succeed (401), 11th from the same IP returns 429 2. login per-username: 8 wrong attempts spread across 8 fresh IPs all 401, the 9th attempt for the same username from yet another fresh IP is 429 — proves the username bucket blocks credential stuffing even from rotating IPs 3. forgot-password per-IP: 5 succeed, 6th 429 4. register per-IP: 5 attempts processed, 6th 429 5. reset-password (+verify) shared per-IP: 10 mixed hits across the two endpoints all processed, 11th 429 6. loopback regression: 15 login attempts from loopback (no X-Forwarded-For) all return non-429, proving the dev skip works and existing tests are unaffected Test results ------------ - New file: 6/6 pass. - Full api-server suite: 327/329 pass. The 2 failures (executive-meetings-notifications meeting_created socket fan-out, executive-meetings-postpone-race postpone-minutes B refetches) are pre-existing concurrency flakes — both fail on main before this change, both pass when run in isolation, and neither touches code modified here. Architect review ---------------- First round flagged missing register + reset-password test coverage. Both added in this commit. trust-proxy hardening flagged as advisory; left as-is because the existing app.ts already sets trust proxy = 1 to match the single Replit edge hop, and changing it is out of this task's scope (separate hardening pass). Residual risk ------------- - Per-IP buckets rely on app.set("trust proxy", 1) in app.ts. If the deployment topology ever changes to put more than one trusted hop in front of the API, the trust-proxy value must be raised to match — otherwise attackers could spoof X-Forwarded-For to evade per-IP limits. - The username-bucket key is normalized (trim().toLowerCase()) but does not collapse Unicode homoglyphs. Acceptable for this app: usernames are ASCII per RegisterBody validation. Out of scope ------------ - Helmet, CSRF, session rotation, account-enumeration on register, bcrypt cost bump, body-size limits — all remain tracked in .local/security/manual-review.md as Medium/Low items.