mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
chi v5.3.0 deprecates middleware.RealIP (IP-spoofing advisories), which failed the Lint and Static Security Analysis CI jobs (SA1019). Replace the RealIP wrapper with chi's middleware.ClientIP: ClientIPFromRemoteAddr is always applied so middleware.GetClientIP is populated, and when trust_forwarded_headers is set and the immediate peer is a trusted-proxy CIDR, ClientIPFromXFF resolves the real client from X-Forwarded-For (rightmost entry outside the trusted CIDRs). The immediate-peer trust gate is preserved, so a non-trusted peer's XFF is ignored. CIDR strings are validated with netip.ParsePrefix first to avoid ClientIPFromXFF's panic. Behavior change: only X-Forwarded-For is honored now (RealIP also read X-Real-IP / True-Client-IP). Docs and a release note follow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
140 lines
4.3 KiB
Go
140 lines
4.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
)
|
|
|
|
// defaultTrustedProxyCIDRs is the safe-by-default list applied when
|
|
// Settings.TrustedProxyCIDRs is empty. Only loopback addresses are trusted —
|
|
// i.e. a reverse proxy on the same host. Anyone deploying behind a proxy on a
|
|
// different host must override this in settings.json.
|
|
var defaultTrustedProxyCIDRs = []string{
|
|
"127.0.0.0/8",
|
|
"::1/128",
|
|
}
|
|
|
|
// clientIPMiddleware resolves the client IP into the request context (read via
|
|
// middleware.GetClientIP). The socket peer is always recorded. When
|
|
// trustForwardedHeaders is set AND the immediate TCP peer is one of trustedPeers,
|
|
// the X-Forwarded-For chain is consulted (chi walks it right-to-left, skipping
|
|
// trustedCIDRStrings, taking the first untrusted entry). On a flat LAN a
|
|
// non-trusted peer's XFF is ignored, so a malicious speaker can't spoof its IP.
|
|
func clientIPMiddleware(trustForwardedHeaders bool, trustedPeers []*net.IPNet, trustedCIDRStrings []string) func(http.Handler) http.Handler {
|
|
base := middleware.ClientIPFromRemoteAddr
|
|
|
|
if !trustForwardedHeaders || len(trustedPeers) == 0 {
|
|
return func(next http.Handler) http.Handler { return base(next) }
|
|
}
|
|
|
|
xff := middleware.ClientIPFromXFF(trustedCIDRStrings...)
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
trusted := base(xff(next)) // peer set first, XFF overrides when found
|
|
untrusted := base(next) // peer only
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if isFromTrustedPeer(r.RemoteAddr, trustedPeers) {
|
|
trusted.ServeHTTP(w, r)
|
|
|
|
return
|
|
}
|
|
|
|
untrusted.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// isFromTrustedPeer reports whether remoteAddr (in the host:port shape that
|
|
// net/http populates) is contained in any of the supplied CIDR blocks.
|
|
func isFromTrustedPeer(remoteAddr string, trustedPeers []*net.IPNet) bool {
|
|
host, _, err := net.SplitHostPort(remoteAddr)
|
|
if err != nil {
|
|
host = remoteAddr
|
|
}
|
|
|
|
ip := net.ParseIP(host)
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
|
|
for _, n := range trustedPeers {
|
|
if n.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ParseTrustedProxyCIDRs converts string CIDRs into *net.IPNet values, falling
|
|
// back to defaultTrustedProxyCIDRs when the input is empty. An invalid CIDR
|
|
// in the input list is reported as an error and stops parsing — better to
|
|
// fail loud than silently fall back.
|
|
func ParseTrustedProxyCIDRs(cidrs []string) ([]*net.IPNet, error) {
|
|
if len(cidrs) == 0 {
|
|
cidrs = defaultTrustedProxyCIDRs
|
|
}
|
|
|
|
out := make([]*net.IPNet, 0, len(cidrs))
|
|
|
|
for _, c := range cidrs {
|
|
_, n, err := net.ParseCIDR(c)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid trusted proxy CIDR %q: %w", c, err)
|
|
}
|
|
|
|
out = append(out, n)
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// validateCIDRStringsForXFF checks that every CIDR string can be parsed by
|
|
// netip.ParsePrefix, which is what middleware.ClientIPFromXFF uses internally
|
|
// (it calls netip.MustParsePrefix and panics on failure). Returns an error
|
|
// listing the first bad entry, so the caller can fall back to peer-only mode
|
|
// rather than panicking at startup.
|
|
func validateCIDRStringsForXFF(cidrs []string) error {
|
|
for _, c := range cidrs {
|
|
if _, err := netip.ParsePrefix(c); err != nil {
|
|
return fmt.Errorf("CIDR %q is not valid for ClientIPFromXFF: %w", c, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// buildClientIPMiddleware is the server-level helper that reads settings and
|
|
// returns a ready-to-use chi middleware. It is split out of ClientIPMiddleware
|
|
// so tests can drive the logic without a full Server.
|
|
func buildClientIPMiddleware(trustForwardedHeaders bool, cidrStrings []string) func(http.Handler) http.Handler {
|
|
if !trustForwardedHeaders {
|
|
return clientIPMiddleware(false, nil, nil)
|
|
}
|
|
|
|
if len(cidrStrings) == 0 {
|
|
cidrStrings = defaultTrustedProxyCIDRs
|
|
}
|
|
|
|
// Validate for netip.MustParsePrefix (panic guard).
|
|
if err := validateCIDRStringsForXFF(cidrStrings); err != nil {
|
|
log.Printf("[ClientIP] invalid trusted_proxy_cidrs: %v — falling back to peer-only", err)
|
|
return clientIPMiddleware(false, nil, nil)
|
|
}
|
|
|
|
// Parse for the peer gate (net.IPNet).
|
|
cidrs, err := ParseTrustedProxyCIDRs(cidrStrings)
|
|
if err != nil {
|
|
log.Printf("[ClientIP] invalid trusted_proxy_cidrs: %v — falling back to peer-only", err)
|
|
return clientIPMiddleware(false, nil, nil)
|
|
}
|
|
|
|
return clientIPMiddleware(true, cidrs, cidrStrings)
|
|
}
|