fix: Audit-Bugfixes (Auth/WAF/Firewall/Cluster/Renderer) — v1.2.94
Verifizierte Bugs aus dem Code-Audit behoben (je mit Test/Build/nft -c geprüft):
- session: IssueWithRoleTTL mutierte geteiltes s.TTL (Data-Race + falsche TTL) → interne issue(); -race-Test.
- auth: Fallback/Federation leiteten role/TOTP nicht aus DB ab (2FA-Bypass auf Secondary, Rolle aus Remote) → viaDB-Flag + DB-Re-Lookup.
- waf: TrustedProxies waren No-op (bogus-Direktive) → XFF-Auflösung im SPOE-Agent (rightmostXFF/ipMatchesAny); RuleExclusions/TrustedProxies validiert (Direktiven-Injection); GetForHost via net.SplitHostPort.
- firewall: Auto-Rule mit IPv6-DstIP erzeugte 'ip daddr <v6>' → bricht ganzes nft-Ruleset; jetzt familienbewusst (ip/ip6, ungültige raus).
- kea: 'interfaces': null bei 0 Subnets → leeres Array.
- cluster_repair: nodeHasPublication schluckte DB-Fehler (Resync auf falschem Node) → (bool,error) fail-closed; IPv6-Primary-URL via net.JoinHostPort.
- cluster_replication: Replikations-Passwort via stdin statt psql -c (nicht mehr in argv/Logs).
- wireguard: Config (Private Key) jetzt configgen.AtomicWrite VOR Symlink/enable; SkipReload-Feld.
- render.go: --no-reload jetzt für alle Renderer (squid/unbound/chrony/wireguard).
- radius: leeres Secret/Passwort + Newlines abgelehnt; freeradius confEscape strippt CR/LF.
- configorch: continue-on-error + errors.Join statt Abbruch mitten in der Sequenz.
- i18n: fehlender Key common.status (de/en).
Verworfen als kein Bug: WAF detection-'blocked' (DetectionOnly liefert keine Interruption), render secrets.New('') (nutzt Default-Masterkey), FanOut-Sort (nur Kommentar), pg_hba (durch nft abgesichert).
Offen/bewusst zurückgestellt (low/risk): AlertWriter-Close (langlebiger Worker, vernachlässigbar), Rolling-Update-Kleinkram (sudoers-gebundener Script-Pfad / GET-State).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package waf
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -75,6 +76,15 @@ func (a *SPOEAgent) handle(ctx context.Context, w *encoding.ActionWriter, m *enc
|
||||
return // WAF not configured or disabled for this domain
|
||||
}
|
||||
|
||||
// Trusted-Proxy-Handling: stammt die Verbindung von einem konfigurierten
|
||||
// Trusted-Proxy, ist die echte Client-IP das letzte X-Forwarded-For-Glied
|
||||
// (das der Proxy angehängt hat), nicht die Proxy-IP selbst.
|
||||
if clientIP != "" && len(de.TrustedProxies) > 0 && ipMatchesAny(clientIP, de.TrustedProxies) {
|
||||
if real := rightmostXFF(rawHdrs); real != "" {
|
||||
clientIP = real
|
||||
}
|
||||
}
|
||||
|
||||
tx := de.WAF.NewTransaction()
|
||||
defer func() {
|
||||
tx.ProcessLogging()
|
||||
@@ -165,6 +175,53 @@ func (a *SPOEAgent) sendAlert(host, clientIP, method, uri string, mr types.Match
|
||||
})
|
||||
}
|
||||
|
||||
// rightmostXFF gibt den letzten (vom nächstgelegenen Proxy angehängten)
|
||||
// X-Forwarded-For-Eintrag zurück, sofern es eine gültige IP ist.
|
||||
func rightmostXFF(rawHdrs string) string {
|
||||
var val string
|
||||
for _, line := range strings.Split(rawHdrs, "\n") {
|
||||
line = strings.TrimRight(line, "\r")
|
||||
idx := strings.IndexByte(line, ':')
|
||||
if idx <= 0 {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(line[:idx]), "x-forwarded-for") {
|
||||
val = strings.TrimSpace(line[idx+1:]) // letzter XFF-Header gewinnt
|
||||
}
|
||||
}
|
||||
if val == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(val, ",")
|
||||
cand := strings.TrimSpace(parts[len(parts)-1])
|
||||
if net.ParseIP(cand) == nil {
|
||||
return ""
|
||||
}
|
||||
return cand
|
||||
}
|
||||
|
||||
// ipMatchesAny prüft, ob ip exakt einer IP oder einem CIDR aus list entspricht.
|
||||
func ipMatchesAny(ip string, list []string) bool {
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed == nil {
|
||||
return false
|
||||
}
|
||||
for _, e := range list {
|
||||
e = strings.TrimSpace(e)
|
||||
if e == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(e, "/") {
|
||||
if _, n, err := net.ParseCIDR(e); err == nil && n.Contains(parsed) {
|
||||
return true
|
||||
}
|
||||
} else if pe := net.ParseIP(e); pe != nil && pe.Equal(parsed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseHeaders splits HAProxy raw headers ("Name: value\r\n…") and
|
||||
// calls fn for each valid header line.
|
||||
func parseHeaders(raw string, fn func(name, val string)) {
|
||||
|
||||
Reference in New Issue
Block a user