ratelimit: key on rightmost X-Forwarded-For entry (fixes #85)
CI / test (pull_request) Successful in 21s
CI / docker (pull_request) Skipped

This commit is contained in:
agent
2026-09-09 10:57:59 -05:00
parent 7ca1b58362
commit 3d1d5ee1fd
2 changed files with 106 additions and 1 deletions
+21 -1
View File
@@ -48,8 +48,28 @@ func (l *limiter) allow(key string, rate, burst float64) bool {
return true
}
// clientIP extracts the request IP (no reverse proxy header by default).
// clientIP extracts the client IP for rate-limit keying (#85).
//
// Trust boundary: palette runs behind exactly ONE trusted reverse proxy
// (Traefik in the k3s pod network). Traefik APPENDS the real client IP to
// X-Forwarded-For, so the RIGHTMOST entry is the last value the trusted
// proxy observed and is unspoofable by the client (a client-supplied fake
// entry only lands on the LEFT and is ignored). This matches chi's
// middleware.RealIP semantics for a single trusted proxy hop.
//
// Direct connections (no XFF header) fall back to RemoteAddr. Directly
// reachable deployments must NOT expose the app to untrusted networks
// without a proxy in front, or attackers could forge the rightmost entry.
func clientIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
if i := strings.LastIndex(xff, ","); i >= 0 {
return strings.TrimSpace(xff[i+1:])
}
return strings.TrimSpace(xff)
}
if xr := r.Header.Get("X-Real-Ip"); xr != "" {
return strings.TrimSpace(xr)
}
host := r.RemoteAddr
if i := strings.LastIndex(host, ":"); i > 0 {
host = host[:i]