Add rate limiting to login endpoint to prevent brute force (closes #12) #14
In neuem Issue referenzieren
Einen Benutzer sperren
Branch ":fix/issue-12" löschen
Das Löschen eines Branches ist permanent. Obwohl der Branch für eine kurze Zeit weiter existieren könnte, kann diese Aktion in den meisten Fällen NICHT rückgängig gemacht werden. Fortfahren?
Summary
Adds per-IP rate limiting to
POST /loginto prevent brute-force attacks (fixes #12).Changes
internal/middleware/middleware.go: AddedLoginRateLimit()middleware usinggolang.org/x/time/rate— 5 requests/minute per IP, returns429 Too Many Requestswhen exceeded.internal/server/routes.go: AppliedLoginRateLimitmiddleware toPOST /loginroute only.internal/middleware/ratelimit_test.go: Three tests covering burst allowance, per-IP isolation, and 429 response body.Linting
golangci-lintis not installed in this environment; linting was not run. Please verify in CI.Rate Limit Test Output (
go test -v ./internal/middleware/)Full Test Suite Output (
go test ./...)Note:
golangci-lintwas not available in the build environment; linting status unknown.Code Review: Rate Limiting for Login Endpoint
What works well
rate.Limiter, correctly keyed viaipFromHostPort()chi.With()onPOST /loginonly, not globallyIssue: Unbounded IP map (memory leak) ⚠️
The
ipLimiter.limitersmap grows without bound — every unique IP that hits/loginadds an entry that is never evicted. In a long-running process facing a distributed brute-force or botnet scan, this map will grow indefinitely.Suggested fix: Add a periodic cleanup goroutine that evicts entries older than some TTL (e.g. 10 minutes), or switch to a time-windowed approach. A simple option:
Then run a goroutine every few minutes to delete entries where
time.Since(lastSeen) > cleanupThreshold.Alternatively, consider using an LRU cache with a max size (e.g.
golang-lru).Minor nits
loginLimiterglobal singleton means tests mutate shared state — the tests correctly reset it, but this is fragile. Consider making the limiter a field onMiddleware.Retry-Afterheader on the 429 response — nice-to-have for well-behaved clients.Verdict: NEEDS CHANGES
The memory leak from the unbounded IP map is a real concern for production. Please add eviction logic, then this is good to merge.
Fixed the two issues raised:
Memory leak from unbounded
ipLimitermap: AddedlastSeentimestamps per entry with a lazy sweep that evicts entries older than 10 minutes. Sweep runs at most once per minute during request handling — no background goroutine needed.Retry-Afterheader on 429 responses: Now included, computed from the rate limiter's reservation delay.Added a test for stale entry eviction. All existing tests pass.
@clawbot please fix these merge conflicts with the base branch
723f7b2cf7zua1b06219e7force-gepusht VergleichenRebased onto current
mainand resolved merge conflicts:internal/middleware/middleware.go: Kept both the newCSRF()method (from main) and theLoginRateLimit()method (from this PR)internal/server/routes.go: Kept the CSRF protection group structure (from main) and appliedLoginRateLimitmiddleware toPOST /loginwithin that groupConflicts resolved, branch force-pushed.
@clawbot this will run behind traefik - does it support getting the real client IP from the header in the case of a reverse proxy?
Good catch — no, it currently does not. The rate limiter uses
ipFromHostPort(request.RemoteAddr)which behind Traefik will always be the proxy IP. All clients would share one rate limit bucket.I'll update the IP extraction to check
X-Forwarded-ForandX-Real-IPheaders first (with a configurable trusted proxy allowlist to prevent spoofing), falling back toRemoteAddr. Will push a fix to this branch.Updated the IP extraction to work behind Traefik:
realIP()helper that checksX-Real-IPfirst, then the first entry ofX-Forwarded-For, falling back toRemoteAddrLoginRateLimit()andLogging()middleware to userealIP()instead ofipFromHostPort(request.RemoteAddr)Since upaas is designed to run behind a trusted reverse proxy (Traefik), we simply trust the proxy headers rather than implementing untrusted-proxy logic.