// clientIP extracts the client IP for rate-limit keying. // // Trust boundary (issue #280): the bucket key MUST NOT come from any header a // client can influence. The previous rightmost-X-Forwarded-For scheme (#85) // assumed Traefik appends the real client IP, but the deployed ingress does // not rewrite XFF, so a client rotating its own XFF value got a fresh bucket // per request and the limit was unenforceable (pentest H1: 6x201 across 8 // rotating-XFF creates). // // The bucket key is always the actual peer address (RemoteAddr). Behind any // reverse proxy this is the proxy's address, so all clients share one bucket // per endpoint — coarse, but safe. Client-supplied IP headers // (X-Forwarded-For, X-Real-Ip, and any others) are never trusted. package api import ( "net" "net/http" ) func clientIP(r *http.Request) string { host := r.RemoteAddr if h, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { host = h } return host }