Skip to content

security: fix rate limiter bypass, error swallowing, race, window math (#2, #4, #17, #18)#587

Merged
lakhansamani merged 1 commit into
mainfrom
security/rate-limiter-fixes
Apr 7, 2026
Merged

security: fix rate limiter bypass, error swallowing, race, window math (#2, #4, #17, #18)#587
lakhansamani merged 1 commit into
mainfrom
security/rate-limiter-fixes

Conversation

@lakhansamani
Copy link
Copy Markdown
Contributor

Summary

Four related rate limiter fixes:

#2 — X-Forwarded-For bypass (High)
gin's c.ClientIP() trusts proxy headers by default and there was no SetTrustedProxies() call anywhere. Any client could spoof X-Forwarded-For to evade per-IP rate limiting and pollute audit logs with arbitrary IPs.

#4 — Redis swallowed errors (High)
internal/rate_limit/redis.go returned (true, nil) on any Redis error. The operator-controlled --rate-limit-fail-closed flag was a no-op because the error never reached the middleware.

#17 — Data race on lastSeen (Low)
The race detector tripped on the per-IP entry's lastSeen field.

#18 — Window math mismatch (Low)
window := burst / rps used integer division and truncated to 0 when burst < rps.

Changes

  • Add Config.TrustedProxies ([]string of CIDRs) and CLI flag --trusted-proxies (empty default → trust no proxies → use RemoteAddr).
  • NewRouter() now calls router.SetTrustedProxies(...). AppConfig added to server.Dependencies for this.
  • redis.go::Allow propagates errors so the middleware can apply fail-closed as configured.
  • lastSeen is now atomic.Int64 (unix nanos) with touch() / lastSeenTime() helpers; cleanup reads via the atomic.
  • Window math uses ceiling division (a + b - 1) / b so the redis sliding window is at least as long as the rps period.

Migration

Operators behind a real reverse proxy must explicitly opt in via --trusted-proxies=<proxy-cidr> or the rate limiter will key on the proxy IP. Document in CHANGELOG and ../authorizer-docs.

Test plan

  • go build ./...
  • go test -race ./internal/rate_limit/...
  • TEST_DBS=sqlite go test -run TestSignup ./internal/integration_tests/
#2, #4, #17, #18)

#2 — X-Forwarded-For bypass
gin's c.ClientIP() trusts proxy headers by default and there was no
SetTrustedProxies() call anywhere. Any client could spoof X-Forwarded-For
to evade per-IP rate limiting and pollute audit logs with arbitrary IPs.

Fix:
- Add Config.TrustedProxies ([]string of CIDRs)
- New CLI flag --trusted-proxies (empty default → trust no proxies → use
  RemoteAddr; safe out of the box)
- NewRouter() now calls router.SetTrustedProxies(...). Operators behind
  a real reverse proxy must explicitly opt in.
- AppConfig added to server.Dependencies so the router knows the trusted
  list at construction time.

#4 — Redis rate limiter swallowed errors
redis.go::Allow returned (true, nil) on any Redis error, "failing open"
silently. The operator-controlled --rate-limit-fail-closed flag was a
no-op because the error never reached the middleware that checks it.

Fix: propagate the error to the caller so the middleware can apply
fail-closed behaviour as configured.

#17 — In-memory rate limiter data race on lastSeen
The lastSeen field on the per-IP entry was a plain time.Time accessed
concurrently by Allow() and the cleanup goroutine. Race detector tripped
and updates were non-deterministic.

Fix: convert lastSeen to atomic.Int64 (unix nanos) with touch() and
lastSeenTime() helpers. cleanup() reads via lastSeenTime() so the lock-
free fast path stays correct.

#18 — Redis window math mismatch vs in-memory
window := burst / rps used integer division and truncated to 0 when
burst < rps, producing inconsistent enforcement across the two backends.

Fix: use ceiling division (a + b - 1) / b so the redis sliding window is
at least as long as the rps period, matching the effective window of the
golang.org/x/time/rate limiter used by the in-memory backend.
@lakhansamani lakhansamani merged commit 681bbac into main Apr 7, 2026
@lakhansamani lakhansamani deleted the security/rate-limiter-fixes branch April 7, 2026 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant