fix(tracker-go): key login rate limiter on X-Real-IP, not spoofable XFF

nginx's X-Forwarded-For is $proxy_add_x_forwarded_for, which only appends
to whatever the client sends — a forged leftmost hop passed straight
through, letting an attacker rotate spoofed values to dodge the 5s login
cooldown. X-Real-IP is set by nginx to $remote_addr on every proxied
location and can't be forged by the client, so key on that instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-04 22:43:43 +02:00
parent db534ea389
commit 5f7b4dcd55
2 changed files with 49 additions and 4 deletions

View file

@ -46,12 +46,22 @@ func (s *Server) handleLoginGet(w http.ResponseWriter, r *http.Request) {
s.serveStaticFile(w, r, "login.html")
}
// loginClientIP returns the rate-limit key for a login attempt. Every nginx
// location proxying to the tracker sets X-Real-IP to $remote_addr (see
// nginx/overlord.conf), which the client cannot forge. X-Forwarded-For is
// $proxy_add_x_forwarded_for — nginx only APPENDS to it, so a client-supplied
// leftmost hop survives untouched and would let an attacker rotate spoofed
// values to dodge the cooldown.
func loginClientIP(r *http.Request) string {
if ip := strings.TrimSpace(r.Header.Get("X-Real-IP")); ip != "" {
return ip
}
return clientIP(r)
}
// POST /login — authenticate and set the session cookie (main.py:login).
func (s *Server) handleLoginPost(w http.ResponseWriter, r *http.Request) {
ip := clientIP(r)
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
ip = strings.TrimSpace(strings.Split(xff, ",")[0])
}
ip := loginClientIP(r)
if !s.loginLimiter.allow(ip) {
writeJSON(w, http.StatusTooManyRequests, map[string]any{"detail": "Too many login attempts. Try again in a few seconds."})
return