Pentest 49.1 LOW: Re-Auth jetzt auf JEDE portalUrl-Änderung
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) <noreply@anthropic.com>
This commit is contained in:
@@ -25,9 +25,13 @@ async function requireCallerPasswordReAuth(req: AuthRequest, providedPassword: u
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
function hostOf(url: string | null | undefined): string | null {
|
// Pentest 49.1 (LOW, 2026-06-01): nur Host-Vergleich ließ Pfad-Änderungen
|
||||||
if (!url) return null;
|
// am gleichen Host (z.B. `https://1und1.de/neue/pfad`) ohne Re-Auth
|
||||||
try { return new URL(url).host.toLowerCase(); } catch { return null; }
|
// 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<void> {
|
export async function getProviders(req: Request, res: Response): Promise<void> {
|
||||||
@@ -107,15 +111,13 @@ export async function updateProvider(req: Request, res: Response): Promise<void>
|
|||||||
const providerId = parseInt(req.params.id);
|
const providerId = parseInt(req.params.id);
|
||||||
const { currentPassword, ...providerData } = req.body || {};
|
const { currentPassword, ...providerData } = req.body || {};
|
||||||
|
|
||||||
// 47.1: portalUrl-Domain-Wechsel braucht Re-Auth. Wir laden den
|
// 47.1 + 49.1: jede portalUrl-Änderung braucht Re-Auth – inkl. reiner
|
||||||
// bisherigen Wert und vergleichen den Host – nur dann ist es ein
|
// Pfad-Änderungen am gleichen Host (Phishing-Pfade auf trusted Domain).
|
||||||
// sensible Operation. Reine Namens-/Tarif-Edits bleiben friction-frei.
|
// Reine Namens-/Tarif-Edits bleiben friction-frei.
|
||||||
if (providerData.portalUrl !== undefined) {
|
if (providerData.portalUrl !== undefined) {
|
||||||
const before = await providerService.getProviderById(providerId);
|
const before = await providerService.getProviderById(providerId);
|
||||||
const oldHost = hostOf(before?.portalUrl);
|
const isUrlChange = normalizeUrlForCompare(before?.portalUrl) !== normalizeUrlForCompare(providerData.portalUrl);
|
||||||
const newHost = hostOf(providerData.portalUrl);
|
if (isUrlChange) {
|
||||||
const isDomainChange = (newHost && newHost !== oldHost) || (oldHost && !newHost);
|
|
||||||
if (isDomainChange) {
|
|
||||||
const reauth = await requireCallerPasswordReAuth(req as AuthRequest, currentPassword);
|
const reauth = await requireCallerPasswordReAuth(req as AuthRequest, currentPassword);
|
||||||
if (!reauth.ok) {
|
if (!reauth.ok) {
|
||||||
res.status(reauth.status).json({ success: false, error: reauth.error } as ApiResponse);
|
res.status(reauth.status).json({ success: false, error: reauth.error } as ApiResponse);
|
||||||
|
|||||||
@@ -299,15 +299,14 @@ function ProviderModal({
|
|||||||
currentPassword: '',
|
currentPassword: '',
|
||||||
});
|
});
|
||||||
const originalPortalUrl = provider?.portalUrl ?? '';
|
const originalPortalUrl = provider?.portalUrl ?? '';
|
||||||
const hostOf = (u: string) => {
|
// Pentest 49.1: Jede URL-Änderung (inkl. Pfad/Query) braucht Re-Auth –
|
||||||
try { return new URL(u.trim()).host.toLowerCase(); } catch { return ''; }
|
// nicht nur Host-Wechsel. Normalisierung (Trailing-Slash, Whitespace,
|
||||||
};
|
// Case) passend zum Backend, damit der Banner mit der Backend-Prüfung
|
||||||
const portalUrlHostChanged =
|
// übereinstimmt.
|
||||||
formData.portalUrl.trim() !== originalPortalUrl.trim()
|
const normalizeUrl = (u: string) => u.trim().replace(/\/+$/, '').toLowerCase();
|
||||||
&& (hostOf(formData.portalUrl) || hostOf(originalPortalUrl))
|
const portalUrlChanged = normalizeUrl(formData.portalUrl) !== normalizeUrl(originalPortalUrl);
|
||||||
&& hostOf(formData.portalUrl) !== hostOf(originalPortalUrl);
|
|
||||||
const portalUrlSetOnCreate = !provider && !!formData.portalUrl.trim();
|
const portalUrlSetOnCreate = !provider && !!formData.portalUrl.trim();
|
||||||
const needsReAuth = portalUrlHostChanged || portalUrlSetOnCreate;
|
const needsReAuth = portalUrlChanged || portalUrlSetOnCreate;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@@ -400,8 +399,8 @@ function ProviderModal({
|
|||||||
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg space-y-2">
|
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg space-y-2">
|
||||||
<p className="text-sm text-amber-800">
|
<p className="text-sm text-amber-800">
|
||||||
<strong>Bestätigung erforderlich:</strong>{' '}
|
<strong>Bestätigung erforderlich:</strong>{' '}
|
||||||
{portalUrlHostChanged
|
{provider
|
||||||
? 'Die Portal-URL-Domain wurde geändert. Diese URL ist anschließend für alle Portal-Kunden dieses Anbieters klickbar.'
|
? '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.'}
|
: '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.
|
{' '}Zur Sicherheit ist eine Bestätigung mit dem eigenen Passwort nötig.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user