MFA fix, added IP blacklist, update webmail client
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"mailgoserver/internal/toolbox"
|
||||
)
|
||||
|
||||
// cloudflareRanges are Cloudflare's published proxy IP ranges (fetched live from
|
||||
// https://www.cloudflare.com/ips-v4 and /ips-v6 rather than trusted from memory,
|
||||
// since a stale list here would either wrongly trust an attacker-controlled hop or
|
||||
// wrongly distrust Cloudflare's own edge) — expanded when "cloudflare" appears in
|
||||
// the trusted_proxies config value. Cloudflare rotates these occasionally; re-fetch
|
||||
// and update this list if IP resolution behind Cloudflare ever looks wrong.
|
||||
var cloudflareRanges = []string{
|
||||
"173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22",
|
||||
"141.101.64.0/18", "108.162.192.0/18", "190.93.240.0/20", "188.114.96.0/20",
|
||||
"197.234.240.0/22", "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22",
|
||||
"2400:cb00::/32", "2606:4700::/32", "2803:f800::/32", "2405:b500::/32",
|
||||
"2405:8100::/32", "2a06:98c0::/29", "2c0f:f248::/32",
|
||||
}
|
||||
|
||||
// parseTrustedProxies reads the [Server] trusted_proxies config value — a
|
||||
// comma-separated list of CIDRs and/or the literal word "cloudflare" — into parsed
|
||||
// prefixes. Unparseable entries are skipped (logged by the caller) rather than
|
||||
// failing startup over a typo in a security-adjacent but non-fatal setting.
|
||||
func parseTrustedProxies(raw string, logger *toolbox.Logger) []netip.Prefix {
|
||||
var out []netip.Prefix
|
||||
for _, entry := range strings.Split(raw, ",") {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(entry, "cloudflare") {
|
||||
for _, cidr := range cloudflareRanges {
|
||||
if p, err := netip.ParsePrefix(cidr); err == nil {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
p, err := netip.ParsePrefix(entry)
|
||||
if err != nil {
|
||||
// A bare IP (no /mask) is a common typo for "trust this one proxy" —
|
||||
// accept it as a /32 or /128 host route rather than silently dropping it.
|
||||
if addr, addrErr := netip.ParseAddr(entry); addrErr == nil {
|
||||
bits := 32
|
||||
if addr.Is6() {
|
||||
bits = 128
|
||||
}
|
||||
out = append(out, netip.PrefixFrom(addr, bits))
|
||||
continue
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Error("trusted_proxies: skipping unparseable entry %q: %v", entry, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isTrustedProxy(trusted []netip.Prefix, addr netip.Addr) bool {
|
||||
for _, p := range trusted {
|
||||
if p.Contains(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// requestIP returns the best-effort real client IP for r. Forwarded headers
|
||||
// (CF-Connecting-IP, X-Forwarded-For, X-Real-IP) are only honored when the direct
|
||||
// TCP peer (r.RemoteAddr) is itself a configured trusted proxy — otherwise a client
|
||||
// with no proxy in front of it could simply set these headers itself and spoof any
|
||||
// IP for every audit log entry and IP-based check in the app. When trusted,
|
||||
// X-Forwarded-For is walked from the right (the hop closest to us) skipping any
|
||||
// entries that are themselves trusted proxies, landing on the first untrusted (i.e.
|
||||
// real client) address — the standard correct algorithm, since the leftmost entry is
|
||||
// client-supplied and trivially spoofable even through a legitimate proxy.
|
||||
func (a *App) requestIP(r *http.Request) string {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
host = r.RemoteAddr
|
||||
}
|
||||
peer, parseErr := netip.ParseAddr(host)
|
||||
if parseErr != nil || !isTrustedProxy(a.trustedProxies, peer) {
|
||||
return host
|
||||
}
|
||||
|
||||
if cf := strings.TrimSpace(r.Header.Get("CF-Connecting-IP")); cf != "" {
|
||||
return cf
|
||||
}
|
||||
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
|
||||
hops := strings.Split(fwd, ",")
|
||||
for i := len(hops) - 1; i >= 0; i-- {
|
||||
hop := strings.TrimSpace(hops[i])
|
||||
if hop == "" {
|
||||
continue
|
||||
}
|
||||
if addr, err := netip.ParseAddr(hop); err == nil && isTrustedProxy(a.trustedProxies, addr) {
|
||||
continue // another hop we also trust — keep walking left for the real client
|
||||
}
|
||||
return hop
|
||||
}
|
||||
}
|
||||
if realIP := strings.TrimSpace(r.Header.Get("X-Real-IP")); realIP != "" {
|
||||
return realIP
|
||||
}
|
||||
return host
|
||||
}
|
||||
Reference in New Issue
Block a user