mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 01:53:33 +00:00
d86211fcdc
* feat(auth): per-email login rate limiter (TASK-651) handleLogin is rate-limited per-IP (5/min, in middleware_ratelimit.go), which is effective against a single attacker but useless against a botnet rotating source IPs to spray one victim's password reset email. Add a second limiter keyed on the lowercased email, 10 attempts/hour burst 10. Consumed inside handleLogin on every attempt (success or failure) — a legitimate user remembers their password within 1-2 tries and never hits the limit, but an attacker pounding one account from 50 IPs is locked out after 10 attempts regardless of where those attempts originate. The blocked attempt is logged to the audit log as ActionLoginFailed with reason=email_rate_limited so admins can see which accounts are being sprayed. Tests: - TestHandleLogin_PerEmailRateLimit exhausts the email limit from 10 distinct IPs, then verifies a fresh-IP attempt against the same email gets 429 while a different email from another fresh IP still gets the ordinary 401. - TestHandleLogin_EmailCaseInsensitive verifies the limiter key is normalized — alternating MIXED@/mixed@/Mixed@ all count against the same bucket. Parent: PLAN-643 (OSS Security Hardening). * fix(auth): retain AuthEmail buckets for 2h per Codex P1 Codex caught that ipRateLimiter's cleanup evicts inactive keys after 30 min, which defeats the 10/hour AuthEmail budget: an attacker bursts 10, waits ~30 min for eviction, bursts another 10 — 20 guesses/hour, not 10. Make retention per-config, and set AuthEmail's to 2 hours (≥ 2x the refill window) so the bucket survives the natural pause between spraying rounds. Per-IP limiters keep the 30-min default since their refill is sub-minute. * fix(auth): bound AuthEmail bucket keys by plausibility per Codex P1 Codex caught that the 2-hour retention window on AuthEmail creates a memory-DoS vector — a distributed attacker can POST many long garbage 'email' strings to /api/v1/auth/login and grow the limiter map without bound, since each call inserts a new bucket before any email validation. Add isPlausibleEmail() pre-filter: reject >254 chars (RFC 5321 cap) and strings without an '@' in the interior. Only plausible emails get a bucket; garbage still gets 401 from the password check below but never makes it into the map. Test: TestHandleLogin_ImplausibleEmail_NoBucketCreated hammers the endpoint with 500-char garbage from many IPs and verifies the AuthEmail map never holds a key starting with that garbage pattern. TestIsPlausibleEmail covers empty, missing @, leading/trailing @, over 254, unicode local part.