Pentest 59.4 HIGH: IPv4-mapped IPv6 in SSRF-Guard blocken (alle Schreibweisen)
Node's URL-Parser normalisiert IPv4-mapped IPv6 von Dotted- in Hex-Form: `::ffff:127.0.0.1` → `::ffff:7f00:1`, `::ffff:169.254.169.254` → `::ffff:a9fe:a9fe` (GCP/AWS-Metadata!), `::ffff:10.0.0.1` → `::ffff:a00:1`. Die bisherigen Patterns (`::ffff:127\.` etc.) matchten nur die Dotted-Form. Sobald die URL durch `new URL()` lief, wurde der Host in Hex-Form herausgereicht und kam an der Blocklist vorbei – live verifiziert auf test-mail-access mit allen drei Payloads. Fix in ssrfGuard.ts: - Neuer extractMappedIPv4-Helper: erkennt Compact-Dotted, Compact-Hex, Expanded-Dotted, Expanded-Hex – konvertiert auf Dotted-IPv4. - Neuer checkIPv4-Helper: läuft die IPv4 durch BLOCKED_PATTERNS und (optional) PRIVATE_IP_PATTERNS, mit BLOCKED/PRIVATE_HOSTNAMES. - isBlockedSsrfHost + isPrivateOrBlockedHost rufen den IPv4-Check bei Mapped-IPv6 zusätzlich auf. Plain IPv4 und Hex-Form werden damit gleich behandelt. Verifiziert mit 15-Tests: ::ffff:7f00:1, ::ffff:a9fe:a9fe, 0:0:0:0:0:ffff:7f00:1 etc. werden alle geblockt; legitime IPs (8.8.8.8, ::ffff:8.8.8.8) bleiben durchlässig. Nebenbefund (Consent-URL = localhost): - getPublicUrl in auth.service jetzt EXPORT (vorher private). - gdpr.controller (sendConsentLink + send-privacy-link) nutzt jetzt getPublicUrl statt direkt PUBLIC_URL/origin/localhost- Kette. Damit greift die admin-konfigurierte AppSetting `portalLoginUrl` auch hier. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import * as gdprService from '../services/gdpr.service.js';
|
||||
import * as consentService from '../services/consent.service.js';
|
||||
import * as consentPublicService from '../services/consent-public.service.js';
|
||||
import * as appSettingService from '../services/appSetting.service.js';
|
||||
import { getPublicUrl } from '../services/auth.service.js';
|
||||
import { canAccessCustomer } from '../utils/accessControl.js';
|
||||
import { createAuditLog, logChange } from '../services/audit.service.js';
|
||||
import { ConsentType, DeletionRequestStatus } from '@prisma/client';
|
||||
@@ -572,7 +573,10 @@ export async function sendConsentLink(req: AuthRequest, res: Response) {
|
||||
|
||||
// ConsentHash sicherstellen
|
||||
const hash = await consentPublicService.ensureConsentHash(customerId);
|
||||
const baseUrl = process.env.PUBLIC_URL || req.headers.origin || 'http://localhost:5173';
|
||||
// 59.4 Nebenbefund: nicht mehr direkt aus env, sondern über
|
||||
// getPublicUrl – nimmt zuerst die admin-konfigurierte AppSetting
|
||||
// `portalLoginUrl`, dann PUBLIC_URL, dann Localhost-Fallback.
|
||||
const baseUrl = await getPublicUrl();
|
||||
const consentUrl = `${baseUrl}/datenschutz/${hash}`;
|
||||
|
||||
// Bei E-Mail: tatsächlich senden
|
||||
@@ -715,7 +719,10 @@ export async function sendAuthorizationRequest(req: AuthRequest, res: Response)
|
||||
return res.status(404).json({ success: false, error: 'Kunde oder Vertreter nicht gefunden' });
|
||||
}
|
||||
|
||||
const baseUrl = process.env.PUBLIC_URL || req.headers.origin || 'http://localhost:5173';
|
||||
// 59.4 Nebenbefund: nicht mehr direkt aus env, sondern über
|
||||
// getPublicUrl – nimmt zuerst die admin-konfigurierte AppSetting
|
||||
// `portalLoginUrl`, dann PUBLIC_URL, dann Localhost-Fallback.
|
||||
const baseUrl = await getPublicUrl();
|
||||
const portalUrl = `${baseUrl}/privacy`;
|
||||
|
||||
// E-Mail senden
|
||||
|
||||
Reference in New Issue
Block a user