/** * Schutz vor Server-Side Request Forgery (SSRF) bei User-kontrollierten * Hosts/URLs in Endpunkten wie test-connection, test-mail-access. * * Wir blockieren bewusst NICHT die komplette private IP-Range (127.0.0.0/8, * 10.0.0.0/8 etc.), weil legitime On-Premise-Setups häufig Plesk/Dovecot/ * Postfix auf 127.0.0.1 oder im internen Netz laufen lassen. Stattdessen * blockieren wir nur: * - Cloud-Metadata-Endpoints (169.254.169.254, fd00:ec2::254) * - 169.254.0.0/16 Link-Local (deckt Cloud-Metadata + APIPA ab) * - 0.0.0.0/8 (ungültiger Source/Routing-Range) * - Multicast / Reserved Ranges (224.0.0.0/4, 240.0.0.0/4) * * Für Defense-in-Depth gegen DNS-Rebinding wäre eine vollständige DNS- * Resolution + IP-Vergleich nötig – das überlassen wir v1.1, weil es * legitimes Caching/CDN-Verhalten brechen kann. */ const BLOCKED_PATTERNS: RegExp[] = [ /^169\.254\./, // Link-Local (AWS/GCP/Azure Metadata, APIPA) /^0\./, // 0.0.0.0/8 reserved /^22[4-9]\./, // 224-229 Multicast /^23[0-9]\./, // 230-239 Multicast /^24[0-9]\./, // 240-249 reserved /^25[0-5]\./, // 250-255 reserved /^fd00:ec2::/i, // AWS IPv6 Metadata /^fe80:/i, // IPv6 Link-Local /^ff/i, // IPv6 Multicast ]; const BLOCKED_HOSTNAMES = new Set([ 'metadata.google.internal', 'metadata.goog', 'metadata', '169.254.169.254', ]); export function isBlockedSsrfHost(host: string | null | undefined): boolean { if (!host) return false; const h = host.trim().toLowerCase(); if (!h) return false; if (BLOCKED_HOSTNAMES.has(h)) return true; for (const pattern of BLOCKED_PATTERNS) { if (pattern.test(h)) return true; } return false; } /** * Wirft einen Fehler, wenn der Host für ausgehende Verbindungen blockiert ist. * Caller sollte den Fehler in 400er Response umsetzen. */ export function assertAllowedHost(host: string | null | undefined, label = 'Host'): void { if (isBlockedSsrfHost(host)) { throw new Error(`${label} verweist auf eine geblockte Adresse (Cloud-Metadata / Link-Local / Reserved).`); } }