Pentest 2026-05-24 Pen-31-Befunde (2x MEDIUM)
31.1 Stored XSS in Vertragsfeldern: providerName, tariffName, priceFirst12Months, priceFrom13Months, priceAfter24Months nahmen rohe HTML-/Script-Payloads (<script>, <svg/onload>, <img onerror>, javascript:, HTML-Entities) an und lieferten sie 1:1 an Portal-User zurueck. Fix: rekursiver sanitizeContractBody()-Walker im contract.controller, strippt String-Werte ueber das bestehende stripHtml() (Tag-Strip + URI-Schema-Block + Entity-Decode). Verträge enthalten keine legitimen HTML-Felder, deshalb safe. Audit-Vergleich nutzt jetzt die sanitisierte Variante, sonst Audit ↔ DB-Drift. 31.2 IDOR auf GET /api/customers/:id/stressfrei-emails (+5 weitere): requireCustomerAccess short-circuitete auf customers:read. Portal- User haben aber genau diese Perm im JWT (für eigene Daten) – damit kam Portal-Kunde 1 an Adressen/Bank-Cards/Documents/Meters/ Stressfrei-Emails von Kunde 3. Fix im Middleware: erst isCustomerPortal-Check (eigene + vertretene IDs), DANN erst Perm-Check für Mitarbeiter. Mit einem Patch alle sechs requireCustomerAccess-Routes dicht. Defense-in-Depth: zusätzlicher canAccessCustomer-Call in stressfreiEmail.getEmailsByCustomer analog zum POST-Handler. Live-verifiziert auf dev: - Portal-User 1 → Customer 3: alle 6 Routes 403 - XSS-Payloads in 5 Contract-Feldern → DB enthält bereinigte Werte Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -158,9 +158,34 @@ export function requireCustomerAccess(
|
||||
return;
|
||||
}
|
||||
|
||||
// WICHTIG: erst die isCustomerPortal-Prüfung, DANN erst die Perm-Prüfung.
|
||||
// Portal-User bekommen `customers:read` im JWT (für eigene Daten); ohne
|
||||
// den Portal-Check vorne weg short-circuited die alte Logik auf der
|
||||
// Perm und ließ Portal-User auf fremde customerId zugreifen.
|
||||
// Pentest 2026-05-24 (MEDIUM 31.2 IDOR auf /api/customers/:id/
|
||||
// stressfrei-emails). Auch andere Routes mit dem gleichen Middleware-
|
||||
// Pattern wären betroffen gewesen.
|
||||
const userPermissions = req.user.permissions || [];
|
||||
const isPortal = !!(req.user as any).isCustomerPortal;
|
||||
const customerId = parseInt(req.params.customerId || req.params.id);
|
||||
|
||||
// Admins and employees can access all customers
|
||||
if (isPortal) {
|
||||
const allowedIds = [
|
||||
req.user.customerId,
|
||||
...((req.user as any).representedCustomerIds || []),
|
||||
].filter(Boolean);
|
||||
if (allowedIds.includes(customerId)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Kein Zugriff auf diese Kundendaten',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Mitarbeiter/Admin: customers:read oder customers:update reicht
|
||||
if (
|
||||
userPermissions.includes('customers:read') ||
|
||||
userPermissions.includes('customers:update')
|
||||
@@ -169,18 +194,6 @@ export function requireCustomerAccess(
|
||||
return;
|
||||
}
|
||||
|
||||
// Customers can only access their own data + represented customers
|
||||
const customerId = parseInt(req.params.customerId || req.params.id);
|
||||
const allowedIds = [
|
||||
req.user.customerId,
|
||||
...((req.user as any).representedCustomerIds || []),
|
||||
].filter(Boolean);
|
||||
|
||||
if (allowedIds.includes(customerId)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Kein Zugriff auf diese Kundendaten',
|
||||
|
||||
Reference in New Issue
Block a user