fix: only trust proxy headers from RFC1918/loopback sources (closes #44)
realIP() now parses RemoteAddr and checks if the source IP is in RFC1918 (10/8, 172.16/12, 192.168/16), loopback (127/8), or IPv6 ULA/loopback ranges before trusting X-Real-IP or X-Forwarded-For headers. Public source IPs have headers ignored (fail closed). This prevents attackers from spoofing X-Forwarded-For to bypass the login rate limiter.
This commit is contained in:
@@ -111,10 +111,53 @@ func ipFromHostPort(hostPort string) string {
|
||||
return host
|
||||
}
|
||||
|
||||
// realIP extracts the client's real IP address from the request,
|
||||
// checking proxy headers first (trusted reverse proxy like Traefik),
|
||||
// then falling back to RemoteAddr.
|
||||
// trustedProxyNets are RFC1918 and loopback CIDRs whose proxy headers we trust.
|
||||
//
|
||||
//nolint:gochecknoglobals // package-level constant nets parsed once
|
||||
var trustedProxyNets = func() []*net.IPNet {
|
||||
cidrs := []string{
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"127.0.0.0/8",
|
||||
"::1/128",
|
||||
"fc00::/7",
|
||||
}
|
||||
|
||||
nets := make([]*net.IPNet, 0, len(cidrs))
|
||||
|
||||
for _, cidr := range cidrs {
|
||||
_, n, _ := net.ParseCIDR(cidr)
|
||||
nets = append(nets, n)
|
||||
}
|
||||
|
||||
return nets
|
||||
}()
|
||||
|
||||
// isTrustedProxy reports whether ip is in an RFC1918, loopback, or ULA range.
|
||||
func isTrustedProxy(ip net.IP) bool {
|
||||
for _, n := range trustedProxyNets {
|
||||
if n.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// realIP extracts the client's real IP address from the request.
|
||||
// Proxy headers (X-Real-IP, X-Forwarded-For) are only trusted when the
|
||||
// direct connection originates from an RFC1918/loopback address.
|
||||
// Otherwise, headers are ignored and RemoteAddr is used (fail closed).
|
||||
func realIP(r *http.Request) string {
|
||||
addr := ipFromHostPort(r.RemoteAddr)
|
||||
remoteIP := net.ParseIP(addr)
|
||||
|
||||
// Only trust proxy headers from private/loopback sources.
|
||||
if remoteIP == nil || !isTrustedProxy(remoteIP) {
|
||||
return addr
|
||||
}
|
||||
|
||||
// 1. X-Real-IP (set by Traefik/nginx)
|
||||
if ip := strings.TrimSpace(r.Header.Get("X-Real-IP")); ip != "" {
|
||||
return ip
|
||||
@@ -130,7 +173,7 @@ func realIP(r *http.Request) string {
|
||||
}
|
||||
|
||||
// 3. Fall back to RemoteAddr
|
||||
return ipFromHostPort(r.RemoteAddr)
|
||||
return addr
|
||||
}
|
||||
|
||||
// CORS returns CORS middleware.
|
||||
|
||||
Reference in New Issue
Block a user