diff --git a/backend/src/controllers/provider.controller.ts b/backend/src/controllers/provider.controller.ts index ffbeceb1..58939432 100644 --- a/backend/src/controllers/provider.controller.ts +++ b/backend/src/controllers/provider.controller.ts @@ -25,9 +25,13 @@ async function requireCallerPasswordReAuth(req: AuthRequest, providedPassword: u return { ok: true }; } -function hostOf(url: string | null | undefined): string | null { - if (!url) return null; - try { return new URL(url).host.toLowerCase(); } catch { return null; } +// Pentest 49.1 (LOW, 2026-06-01): nur Host-Vergleich ließ Pfad-Änderungen +// am gleichen Host (z.B. `https://1und1.de/neue/pfad`) ohne Re-Auth +// durchgehen – ein gestohlener JWT konnte Phishing-Pfade auf trusted +// Domains plazieren. Jetzt vergleichen wir die komplette normalisierte +// URL (Trailing-Slash, Whitespace). +function normalizeUrlForCompare(url: string | null | undefined): string { + return (url ?? '').trim().replace(/\/+$/, '').toLowerCase(); } export async function getProviders(req: Request, res: Response): Promise { @@ -107,15 +111,13 @@ export async function updateProvider(req: Request, res: Response): Promise const providerId = parseInt(req.params.id); const { currentPassword, ...providerData } = req.body || {}; - // 47.1: portalUrl-Domain-Wechsel braucht Re-Auth. Wir laden den - // bisherigen Wert und vergleichen den Host – nur dann ist es ein - // sensible Operation. Reine Namens-/Tarif-Edits bleiben friction-frei. + // 47.1 + 49.1: jede portalUrl-Änderung braucht Re-Auth – inkl. reiner + // Pfad-Änderungen am gleichen Host (Phishing-Pfade auf trusted Domain). + // Reine Namens-/Tarif-Edits bleiben friction-frei. if (providerData.portalUrl !== undefined) { const before = await providerService.getProviderById(providerId); - const oldHost = hostOf(before?.portalUrl); - const newHost = hostOf(providerData.portalUrl); - const isDomainChange = (newHost && newHost !== oldHost) || (oldHost && !newHost); - if (isDomainChange) { + const isUrlChange = normalizeUrlForCompare(before?.portalUrl) !== normalizeUrlForCompare(providerData.portalUrl); + if (isUrlChange) { const reauth = await requireCallerPasswordReAuth(req as AuthRequest, currentPassword); if (!reauth.ok) { res.status(reauth.status).json({ success: false, error: reauth.error } as ApiResponse); diff --git a/frontend/src/pages/settings/ProviderList.tsx b/frontend/src/pages/settings/ProviderList.tsx index 0375c451..3c79f193 100644 --- a/frontend/src/pages/settings/ProviderList.tsx +++ b/frontend/src/pages/settings/ProviderList.tsx @@ -299,15 +299,14 @@ function ProviderModal({ currentPassword: '', }); const originalPortalUrl = provider?.portalUrl ?? ''; - const hostOf = (u: string) => { - try { return new URL(u.trim()).host.toLowerCase(); } catch { return ''; } - }; - const portalUrlHostChanged = - formData.portalUrl.trim() !== originalPortalUrl.trim() - && (hostOf(formData.portalUrl) || hostOf(originalPortalUrl)) - && hostOf(formData.portalUrl) !== hostOf(originalPortalUrl); + // Pentest 49.1: Jede URL-Änderung (inkl. Pfad/Query) braucht Re-Auth – + // nicht nur Host-Wechsel. Normalisierung (Trailing-Slash, Whitespace, + // Case) passend zum Backend, damit der Banner mit der Backend-Prüfung + // übereinstimmt. + const normalizeUrl = (u: string) => u.trim().replace(/\/+$/, '').toLowerCase(); + const portalUrlChanged = normalizeUrl(formData.portalUrl) !== normalizeUrl(originalPortalUrl); const portalUrlSetOnCreate = !provider && !!formData.portalUrl.trim(); - const needsReAuth = portalUrlHostChanged || portalUrlSetOnCreate; + const needsReAuth = portalUrlChanged || portalUrlSetOnCreate; useEffect(() => { if (isOpen) { @@ -400,8 +399,8 @@ function ProviderModal({

Bestätigung erforderlich:{' '} - {portalUrlHostChanged - ? 'Die Portal-URL-Domain wurde geändert. Diese URL ist anschließend für alle Portal-Kunden dieses Anbieters klickbar.' + {provider + ? 'Die Portal-URL wurde geändert. Diese URL ist anschließend für alle Portal-Kunden dieses Anbieters klickbar.' : 'Mit dem Speichern wird die Portal-URL für alle Portal-Kunden dieses Anbieters klickbar.'} {' '}Zur Sicherheit ist eine Bestätigung mit dem eigenen Passwort nötig.