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:
2026-05-24 15:38:16 +02:00
co-authored by Claude Opus 4.7
parent 897abc7b21
commit aa0900410b
4 changed files with 103 additions and 20 deletions
+36
View File
@@ -120,6 +120,42 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
- **Live-verifiziert**: 4867 Datensätze + 1 Datei in 13.2s
wiederhergestellt, Log-Modal zeigt den vollständigen Verlauf.
- [x] **🛡️ Pentest 2026-05-24 Pen-31-Befunde (2× MEDIUM)**
- **31.1 Stored XSS in Vertragsfeldern**: `providerName`, `tariffName`,
`priceFirst12Months`, `priceFrom13Months`, `priceAfter24Months`
nahmen rohe HTML/Script-Payloads an und lieferten sie 1:1 zurück.
Fix: rekursiver `sanitizeContractBody()` (Walk-and-Strip) im
contract.controller wird auf `req.body` von POST + PUT
angewandt. Nutzt das bestehende `stripHtml()` aus utils/sanitize,
inkl. URI-Schema-Block + Entity-Decode. Verträge enthalten keine
legitimen HTML-Felder (Editor-HTML lebt in AppSettings), daher
Strip ohne Risiko. Audit-Vergleich nutzt jetzt die sanitisierte
Version, sonst Audit ↔ DB-Drift.
- **31.2 IDOR auf `GET /customers/:id/stressfrei-emails`** (und 4
weiteren Routes mit `requireCustomerAccess`): das Middleware
short-circuitete auf `customers:read` aber Portal-User haben
diese Perm im JWT (für eigene Daten). Damit kam Portal-Kunde 1
an IMAP-Konten/Adressen/Bank-Cards/Documents/Meters von
Kunde 3. Fix in `middleware/auth.ts:requireCustomerAccess`:
erst `isCustomerPortal`-Check (eigene + vertretene IDs), DANN
erst Perm-Check für Mitarbeiter. Damit sind alle 6 Routes
mit einem Middleware-Patch dicht. Defense-in-Depth: in
`stressfreiEmail.controller.getEmailsByCustomer` zusätzlich
`canAccessCustomer`-Call analog zum POST-Handler.
- **Infos** (keine Code-Änderung):
- `type:"STROM"` ist deprecated richtige Enum ist `ELECTRICITY`.
- HSTS auf Staging fehlt: HSTS macht der nginx-Reverse-Proxy,
Backend setzt's bewusst nicht (Doppel-Header-Vermeidung).
Auf Staging muss der Proxy-Op das HSTS-Header-Add aktivieren.
- Portal-Login-Rate-Limit 5 vs 10: Env-Drift, identische Codebase.
- **Live-verifiziert** auf dev:
- Portal-User 1 vs Customer 3: alle 6 Routes 403
(`/customers/3`, `.../addresses`, `.../bank-cards`,
`.../documents`, `.../meters`, `.../stressfrei-emails`).
- XSS-Payloads `<script>`, `<svg/onload>`, `<img onerror>`,
`javascript:`, `&#60;script&#62;` in 5 Vertragsfeldern →
DB-Werte bereinigt (`EvilProvider`, `blocked:alert(4) 35€` etc.).
- [x] **🆕 Vertragsansicht: Kunden-Schnellansicht-Modal + Cent/Euro-Doppel-Input**
- **Info-Icon neben Kundennamen** öffnet ein Modal mit den
wichtigsten Kundendaten (Firma, Name, Geburtsdatum/-ort,