Skip to content

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

Merged
lakhansamani merged 1 commit intomainfrom
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 intomainfrom
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant