Pentest Runde 35 follow-up: portalLoginUrl blockt ALLE privaten IPs
Runde-35-Befund: 34.5 nur teilweise gefixt – Cloud-Metadata (169.254.x.x) wurde blockiert, aber 10/8, 172.16/12, 192.168/16, 127/8 und localhost gingen weiter durch, weil isBlockedSsrfHost diese Ranges nur mit SSRF_BLOCK_PRIVATE_IPS=true geprüft hat. Der Flag steht aber bewusst auf false für on-prem (Plesk auf 127.0.0.1). Threat-Modell-Unterschied: portalLoginUrl ist eine URL in *Endkunden-Mails*. Kunden können 127.0.0.1/192.168.x.x ohnehin nicht erreichen → kein legitimer Wert. Daher muss der Check hier strikt sein, unabhängig vom on-prem-Flag (der gilt nur für ausgehende Server-zu-Server-Verbindungen wie Provider-Test-Connection). Neuer isPrivateOrBlockedHost() in ssrfGuard.ts: union aus BLOCKED_PATTERNS (Metadata/Multicast/Reserved) und PRIVATE_IP_PATTERNS (10/8, 172.16/12, 192.168/16, 127/8, ::1, fc00::/7) + PRIVATE_HOSTNAMES (localhost, ip6-loopback), egal was SSRF_BLOCK_PRIVATE_IPS sagt. portalLoginUrl-Validator nutzt jetzt isPrivateOrBlockedHost + strippt eckige Klammern aus IPv6-Hostnames (Node URL.hostname liefert "[::1]" inkl. Brackets). Live-verifiziert: 22 Test-Cases (9 Private/Loopback, 4 Schemes, 7 legitime). Auch CIDR-Grenzen (172.15 zulässig, 172.16/31 blockiert, 172.32 zulässig). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,31 @@ export function isBlockedSsrfHost(host: string | null | undefined): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strikter Check: blockt private/loopback IP-Ranges UNABHÄNGIG von
|
||||
* `SSRF_BLOCK_PRIVATE_IPS`. Für Use Cases, in denen ein privater Host
|
||||
* NIE legitim sein kann – z.B. eine URL, die an Endkunden per Mail
|
||||
* geht (der Kunde kann eh nicht auf 192.168.x.x routen). Pentest
|
||||
* 2026-05-28 Runde 35.
|
||||
*
|
||||
* Liefert true auch für die regulären Block-Patterns (Cloud-Metadata
|
||||
* etc.), sodass Caller nur eine Funktion aufrufen müssen.
|
||||
*/
|
||||
export function isPrivateOrBlockedHost(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;
|
||||
if (PRIVATE_HOSTNAMES.has(h)) return true;
|
||||
for (const pattern of BLOCKED_PATTERNS) {
|
||||
if (pattern.test(h)) return true;
|
||||
}
|
||||
for (const pattern of PRIVATE_IP_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.
|
||||
|
||||
Reference in New Issue
Block a user