Pentest 48.1 MEDIUM + 50.1 MEDIUM: customerEmailLabel-Strip + SSRF strict
48.1 (XSS in customerEmailLabel):
- Neuer sanitizeCustomerEmailLabel-Helper (stripHtml + trim +
60-Zeichen-Cap)
- Eingesetzt in createProviderConfig + updateProviderConfig
(Write-Pfad) und getProviderPublicSettings (Read-Defensive)
- Damit landet kein <script>/<img onerror>/<svg onload> mehr roh
in der DB, das Längen-Limit ist serverseitig erzwungen, und
Alt-Daten kommen über /public-settings ebenfalls gestrippt raus.
50.1 (SSRF, unvollständige Blockliste bei test-connection):
- safeResolveHost + assertAllowedHost akzeptieren jetzt
{ strict: boolean }. strict=true → isPrivateOrBlockedHost
(sperrt 127/8, 10/8, 172.16/12, 192.168/16, ::1, fc00::/7
unabhängig von SSRF_BLOCK_PRIVATE_IPS).
- test-connection und test-mail-access nutzen strict=true per
Default. Opt-out via env SSRF_ALLOW_INTERNAL_TESTING=true
für On-Prem mit internem Plesk.
- Defense-in-Depth: assertAllowedHost wird jetzt auch VOR der
DNS-Resolution auf den Hostname selbst angewendet, damit
Block-Hostnames (z.B. "metadata.google.internal", "localhost")
nicht via custom-DNS umgangen werden können.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -106,10 +106,17 @@ export function isPrivateOrBlockedHost(host: string | null | undefined): boolean
|
||||
/**
|
||||
* Wirft einen Fehler, wenn der Host für ausgehende Verbindungen blockiert ist.
|
||||
* Caller sollte den Fehler in 400er Response umsetzen.
|
||||
*
|
||||
* `strict=true` (Pentest 50.1, 2026-06-01): private/Loopback-Ranges werden
|
||||
* UNABHÄNGIG von `SSRF_BLOCK_PRIVATE_IPS` immer geblockt. Für Endpunkte mit
|
||||
* besonders kritischer Angriffsfläche (test-connection, test-mail-access),
|
||||
* die im Cloud-Deployment sonst Metadata-/Internal-Service-Probes erlauben
|
||||
* würden.
|
||||
*/
|
||||
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).`);
|
||||
export function assertAllowedHost(host: string | null | undefined, label = 'Host', opts: { strict?: boolean } = {}): void {
|
||||
const blocked = opts.strict ? isPrivateOrBlockedHost(host) : isBlockedSsrfHost(host);
|
||||
if (blocked) {
|
||||
throw new Error(`${label} verweist auf eine geblockte Adresse (Cloud-Metadata / Link-Local / Reserved / privater Host).`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,18 +134,29 @@ import net from 'net';
|
||||
*
|
||||
* Wenn der Host bereits eine IP-Literal ist, wird er direkt geprüft.
|
||||
*/
|
||||
export async function safeResolveHost(host: string | null | undefined, label = 'Host'): Promise<{ ip: string; servername: string }> {
|
||||
export async function safeResolveHost(
|
||||
host: string | null | undefined,
|
||||
label = 'Host',
|
||||
opts: { strict?: boolean } = {},
|
||||
): Promise<{ ip: string; servername: string }> {
|
||||
if (!host || !host.trim()) {
|
||||
throw new Error(`${label} fehlt`);
|
||||
}
|
||||
const trimmed = host.trim();
|
||||
const check = opts.strict ? isPrivateOrBlockedHost : isBlockedSsrfHost;
|
||||
|
||||
// IP-Literal? Direkt prüfen, kein DNS nötig.
|
||||
if (net.isIP(trimmed)) {
|
||||
assertAllowedHost(trimmed, label);
|
||||
assertAllowedHost(trimmed, label, opts);
|
||||
return { ip: trimmed, servername: trimmed };
|
||||
}
|
||||
|
||||
// Pentest 50.1 Defense-in-Depth: bereits vor DNS prüfen, ob der
|
||||
// Hostname selbst auf der Blocklist steht (z.B. "metadata",
|
||||
// "metadata.google.internal", "localhost"). DNS könnte sonst je nach
|
||||
// Resolver legitime IPs liefern und so die Hostname-Blocklist umgehen.
|
||||
assertAllowedHost(trimmed, label, opts);
|
||||
|
||||
// Hostname → resolve to IPv4 + IPv6
|
||||
let ips: string[] = [];
|
||||
try {
|
||||
@@ -155,7 +173,7 @@ export async function safeResolveHost(host: string | null | undefined, label = '
|
||||
|
||||
// Alle aufgelösten IPs prüfen – schon eine geblockte reicht für Ablehnung.
|
||||
for (const ip of ips) {
|
||||
if (isBlockedSsrfHost(ip)) {
|
||||
if (check(ip)) {
|
||||
throw new Error(`${label} ${trimmed} löst auf geblockte Adresse ${ip} auf`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user