Security-Hardening Runde 10: Pentest Runde 6 (8 Findings + struktureller Audit-Sweep)

KRITISCH:
- emails/:id/thread bekommt canAccessCachedEmail
- customers/:customerId/representatives/search bekommt canAccessCustomer
  (Buchstaben-Brute-Force konnte sonst die Kunden-DB enumerieren)

HOCH:
- birthdays/upcoming: Portal-User → 403 (Name/E-Mail/Telefon/Geb-Datum
  aller Kunden leakte)
- contracts/:id/history (GET/POST/PUT/DELETE) bekommt canAccessContract
- mailbox-accounts / unread-count / contracts/:id/emails/folder-counts
  bekommen canAccessCustomer bzw. canAccessContract
- Vertreter-Vollmacht-Check ist jetzt live: neuer Helper
  getPortalAllowedCustomerIds() in accessControl.ts ruft
  hasAuthorization() für jedes vertretene Customer ab. Eingesetzt in
  getTasks/createSupportTicket/createCustomerReply/getAllTasks/
  getTaskStats und updateCustomerConsent. Widerrufene Vollmachten
  haben jetzt SOFORT keinen Zugriff mehr (vorher: bis JWT abläuft).

MITTEL:
- confirmPasswordReset speichert portalPasswordEncrypted nicht mehr
  beim Self-Service-Reset (war nur für Admin-OTPs gedacht); +
  portalPasswordMustChange=false explizit
- getCustomers pagination total reflektiert jetzt nur erlaubte IDs
  (über DB-Filter in customerService.getAllCustomers)

Audit-Sweep (defense in depth, falls Rolle versehentlich Update-
Permissions bekommt):
- 16 cachedEmail-Operationen (markAsRead, toggleStar, assign/unassign,
  save-as-pdf/invoice/contract-document, save-to, attachment-targets,
  trash-ops)
- 4 contract-Operationen (createFollowUp, createRenewal, snoozeContract,
  removeContractMeter)
- 12 sub-CRUD-Operationen (address/bankcard/document/meter
  update+delete, meter-reading add/update/delete/transfer)
- 2 representative-Operationen (add/remove)

Live-verifiziert: Portal-Customer-3 auf alle fremden IDs → 403,
Admin sieht alles, eigene Ressourcen weiterhin 200, Customer 1 mit
widerrufener Vollmacht für Customer 3 → 0 fremde Verträge in der
Response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 23:47:17 +02:00
parent 38c2d82c02
commit a982795388
11 changed files with 256 additions and 137 deletions
+61
View File
@@ -97,6 +97,67 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt
- [x] **🚨 Pentest Runde 6 Sammelfix + Strukturelles Audit (8 Findings + Audit-Sweep)**
- **KRITISCH-01 `GET /emails/:id/thread`**: kein Owner-Check →
Portal-Kunde konnte alle Mail-Threads durchsuchen. Fix:
`canAccessCachedEmail` im Controller.
- **KRITISCH-02 `GET /customers/:customerId/representatives/search`**:
kein `canAccessCustomer` auf den Pfad → DSGVO-GAU, Portal-Kunde
konnte mit Buchstaben-Brute-Force die komplette Kunden-DB
auslesen. Fix eingefügt.
- **HOCH-01 `GET /birthdays/upcoming`**: kein Portal-Filter → Name,
E-Mail, Telefon, Geburtsdatum aller Kunden lesbar. Fix:
`isCustomerPortal` → 403.
- **HOCH-02 `*/contracts/:contractId/history`**: kein Owner-Check
auf GET/POST/PUT/DELETE. Fix: `canAccessContract` in allen vier
History-Handlern.
- **HOCH-03 Mailbox-Endpoints**: `mailbox-accounts`, `unread-count`,
`contracts/:id/emails/folder-counts` ohne Check. Fix:
`canAccessCustomer` bzw. `canAccessContract` in allen drei.
- **HOCH-04 Live-Vollmacht-Check in Tasks**: `getTasks`,
`createSupportTicket`, `createCustomerReply`, `getAllTasks`,
`getTaskStats` prüften nur `representedCustomerIds.includes(...)`
aus dem JWT widerrufene Vollmachten hatten weiter Zugriff
(JWT lebt bis zu 15min nach Widerruf). Neuer Helper
`getPortalAllowedCustomerIds()` in `accessControl.ts` ruft
`hasAuthorization()` live ab. Auch `updateCustomerConsent`
(GDPR) auf diesen Pfad umgestellt.
- **MITTEL-01 `confirmPasswordReset` Klartext-Speicherung**:
Self-Service-Reset speicherte `portalPasswordEncrypted = encrypt(pw)`.
Klartext-Speicherung ist nur für Admin-OTPs sinnvoll. Fix:
Field auf null, zusätzlich `portalPasswordMustChange = false`.
- **MITTEL-02 Pagination-Total leakt globale Kunden-Anzahl**:
`GET /customers` gab `total: 4271` auch wenn Portal-User nur
1 Kunde sah. Fix: `customer.service.ts` erweitert um
`allowedIds`-Filter, der direkt in der DB-Query landet → die
pagination zählt nur über erlaubte IDs.
- **Strukturelles Audit-Sweep** (Sub-CRUD + Email-Operationen):
Folgende Handler bekamen jetzt erstmals einen `canAccess*`-
Check, defense in depth gegen falsch vergebene Rollen:
`markAsRead`, `toggleStar`, `assignToContract`,
`unassignFromContract`, `deleteEmail`, `getTrashEmails`,
`getTrashCount`, `restoreEmail`, `permanentDeleteEmail`,
`getAttachmentTargets`, `saveAttachmentTo`, `saveEmailAsPdf`,
`saveEmailAsInvoice`, `saveAttachmentAsInvoice`,
`saveAttachmentAsContractDocument`, `createFollowUp`,
`createRenewal`, `snoozeContract`, `removeContractMeter`,
`updateAddress`, `deleteAddress`, `updateBankCard`,
`deleteBankCard`, `updateDocument`, `deleteDocument`,
`updateMeter`, `deleteMeter`, `addMeterReading`,
`updateMeterReading`, `deleteMeterReading`,
`markReadingTransferred`, `addRepresentative`,
`removeRepresentative`.
- **Live-verifiziert** (Portal-User Customer 3 auf fremde IDs):
`customers/1/representatives/search` → 403,
`birthdays/upcoming` → 403 (Admin → 200),
`emails/21/thread` → 403,
`customers/2/mailbox-accounts` → 403,
`emails/unread-count?customerId=2` → 403,
`contracts/8/{history,folder-counts,follow-up,renewal,snooze}` → 403,
eigene `customers/3` → 200,
pagination.total für Portal = 1 (statt 3),
Customer 1 mit widerrufener Vollmacht → 0 fremde Verträge.
- [x] **🚨 Pentest Runde 5 KRITISCH: change-initial-portal-password ohne Pflicht-Check**
- **Realer Angriff**: Jeder Portal-User konnte jederzeit mit
seinem eingeloggten Token `POST /api/auth/change-initial-portal-