179 lines
5.6 KiB
Go
179 lines
5.6 KiB
Go
// 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
|
|
}
|