// Dieses File regelt, wann dem Header X-Forwarded-For geglaubt wird. // // # Das Problem // // Läuft das Wiki direkt am Netz, ist r.RemoteAddr die echte IP des Besuchers. // Läuft davor ein Reverse-Proxy (nginx, Caddy, Traefik, Cloudflare), sieht der // Server nur noch die IP des Proxys — für alle Besucher dieselbe. Das // IP-Rate-Limiting (ratelimit.go) wäre damit wirkungslos: entweder es sperrt // niemanden aus, oder es sperrt beim ersten Angreifer gleich alle. // // Der Proxy schickt die echte IP deshalb im Header X-Forwarded-For mit. // // # Warum man diesem Header nicht einfach glauben darf // // Header kann jeder Client frei setzen. Würde der Server X-Forwarded-For // bedingungslos übernehmen, bräuchte ein Angreifer nur bei jedem Login-Versuch // eine andere Fantasie-IP zu schicken — das Rate-Limiting wäre komplett // ausgehebelt, und das Audit-Log stünde voller erfundener Adressen. // // # Die Lösung // // Der Header wird nur ausgewertet, wenn der Request tatsächlich von einem als // vertrauenswürdig konfigurierten Proxy kommt. Konfiguriert wird das über die // Umgebungsvariable TRUSTED_PROXY, z.B.: // // TRUSTED_PROXY=127.0.0.1,10.0.0.0/8 // // Ist sie nicht gesetzt (Standard), wird der Header ignoriert — sicher per Default. package auth import ( "fmt" "net" "net/http" "net/netip" "strings" "sync" ) // trustedProxies hält die konfigurierten Netze. Wird einmal beim Start über // SetTrustedProxies gefüllt und danach nur noch gelesen. // // Ein Paket-globaler Wert ist hier vertretbar, weil clientIP() von sehr vielen // Stellen aufgerufen wird und die Konfiguration prozessweit gilt. Der RWMutex // schützt trotzdem sauber, falls die Konfiguration doch einmal später gesetzt wird. var ( trustedProxiesMu sync.RWMutex trustedProxies []netip.Prefix ) // SetTrustedProxies liest die TRUSTED_PROXY-Konfiguration ein. // // Akzeptiert eine kommaseparierte Liste aus einzelnen IPs ("127.0.0.1") und // CIDR-Netzen ("10.0.0.0/8"). Eine einzelne IP wird intern zu einem Netz mit // voller Präfixlänge — dann gilt für beide Fälle derselbe Vergleich. func SetTrustedProxies(value string) error { var prefixes []netip.Prefix for _, raw := range strings.Split(value, ",") { entry := strings.TrimSpace(raw) if entry == "" { continue } if strings.Contains(entry, "/") { prefix, err := netip.ParsePrefix(entry) if err != nil { return fmt.Errorf("TRUSTED_PROXY: %q ist kein gültiges Netz: %w", entry, err) } prefixes = append(prefixes, prefix) continue } addr, err := netip.ParseAddr(entry) if err != nil { return fmt.Errorf("TRUSTED_PROXY: %q ist keine gültige IP-Adresse: %w", entry, err) } prefixes = append(prefixes, netip.PrefixFrom(addr, addr.BitLen())) } trustedProxiesMu.Lock() defer trustedProxiesMu.Unlock() trustedProxies = prefixes return nil } // TrustedProxyCount gibt zurück wie viele Netze konfiguriert sind — nur damit // main.go beim Start eine sinnvolle Zeile ins Log schreiben kann. func TrustedProxyCount() int { trustedProxiesMu.RLock() defer trustedProxiesMu.RUnlock() return len(trustedProxies) } // ClientIP ermittelt die Adresse des Aufrufers. // // Normalerweise ist das r.RemoteAddr. Steht ein Reverse-Proxy davor, ist das // dessen Adresse — dann wird X-Forwarded-For ausgewertet, aber nur wenn der // Absender über TRUSTED_PROXY als vertrauenswürdig hinterlegt ist. func ClientIP(r *http.Request) string { if ip, ok := forwardedClientIP(r); ok { return ip } host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr } return host } // forwardedClientIP liefert die echte Client-IP aus X-Forwarded-For — aber nur // wenn der direkte Absender ein vertrauenswürdiger Proxy ist. func forwardedClientIP(r *http.Request) (string, bool) { trustedProxiesMu.RLock() prefixes := trustedProxies trustedProxiesMu.RUnlock() if len(prefixes) == 0 { return "", false } // Wer klopft direkt an? Nur dessen Vertrauenswürdigkeit zählt. host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return "", false } peer, err := netip.ParseAddr(host) if err != nil { return "", false } if !isTrustedProxy(peer, prefixes) { return "", false } header := r.Header.Get("X-Forwarded-For") if header == "" { return "", false } // Format: "client, proxy1, proxy2" — ganz links steht der ursprüngliche Client. // // Wir gehen von rechts nach links und überspringen alle vertrauenswürdigen // Proxys; der erste Eintrag der kein bekannter Proxy ist, ist der Client. // Das ist wichtiger als es klingt: würde man stur den linkesten Eintrag nehmen, // könnte ein Client einfach ein eigenes "X-Forwarded-For: 1.2.3.4" mitschicken, // das der Proxy dann nur ergänzt — und die erfundene IP stünde ganz links. parts := strings.Split(header, ",") for i := len(parts) - 1; i >= 0; i-- { candidate := strings.TrimSpace(parts[i]) addr, err := netip.ParseAddr(candidate) if err != nil { // Unlesbarer Eintrag — ab hier ist der Kette nicht mehr zu trauen. return "", false } if isTrustedProxy(addr, prefixes) { continue } return addr.String(), true } // Alle Einträge waren bekannte Proxys — dann bleibt nur der direkte Absender. return "", false } // isTrustedProxy prüft ob eine Adresse in einem der konfigurierten Netze liegt. func isTrustedProxy(addr netip.Addr, prefixes []netip.Prefix) bool { // IPv4-Adressen können in IPv6-Schreibweise ankommen (::ffff:127.0.0.1). // Unmap macht daraus wieder eine normale IPv4-Adresse, sonst schlägt der // Vergleich mit einem IPv4-Netz fehl. addr = addr.Unmap() for _, prefix := range prefixes { if prefix.Contains(addr) { return true } } return false }