From 57eb29c2a60d1ddd064a9fdf2aba20b382b48713 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 1 Jun 2026 13:46:56 +0200 Subject: [PATCH] =?UTF-8?q?Pentest=2049.1=20LOW:=20Re-Auth=20jetzt=20auf?= =?UTF-8?q?=20JEDE=20portalUrl-=C3=84nderung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher prüfte der Re-Auth-Trigger nur den Host – `https://1und1.de/foo` → `https://1und1.de/phishing/path` ging ohne currentPassword durch. Damit konnte ein gestohlener JWT Phishing-Pfade auf trusted Domains plazieren. Backend (provider.controller): normalizeUrlForCompare vergleicht jetzt die komplette URL (Trailing-Slash, Whitespace, Case), nicht nur den Host. hostOf-Helper entfernt. Frontend (ProviderModal): gleiche Normalisierung im UI, damit der Bestätigungs-Banner mit der Backend-Prüfung synchron läuft. Banner-Text leicht angepasst (nicht mehr "Domain wurde geändert" sondern generisch "Portal-URL wurde geändert"). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/controllers/provider.controller.ts | 22 ++++++++++--------- frontend/src/pages/settings/ProviderList.tsx | 19 ++++++++-------- 2 files changed, 21 insertions(+), 20 deletions(-) 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.