Security-Hardening Runde 9: Pentest Runde 5

KRITISCH – change-initial-portal-password ohne mustChange-Pflicht-Check:
Jeder Portal-User konnte jederzeit sein Passwort ohne Kenntnis des
alten ersetzen (XSS-/Token-Hijack-Eskalation). Endpoint war NUR für
den OTP-Erst-Login gedacht, prüfte aber das Flag nicht. Fix: Customer
laden, portalPasswordMustChange=true erzwingen, sonst 403.

NIEDRIG – consentHash leakte über GET /customers/🆔
Hash ist Pseudo-Credential für den öffentlichen Consent-Link. Jetzt
in SENSITIVE_CUSTOMER_FIELDS (sanitize.ts) → wird aus jeder customer-
Response gestrippt. Wer ihn legitim braucht, holt ihn über
/gdpr/customer/:id/consent-status.

NIEDRIG – Public consent-grant Response leakte CustomerConsent-Records:
POST /api/public/consent/:hash/grant gab volle Records inkl. ipAddress
und createdBy (Kunden-Name) zurück. Auf { granted: <count> } reduziert
– Frontend liest eh nur success.

Live-verifiziert:
- Change-Initial ohne Flag → 403; mit Flag → 200; danach Flag=false →
  erneuter Aufruf 403
- GET /customers/3 → consentHash null, portalPasswordHash null
- /gdpr/customer/3/consent-status → consentHash weiterhin sichtbar
- Public-Grant-Response: {granted: 4}, keine ipAddress/createdBy

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 22:57:09 +02:00
parent 75c833500e
commit 38c2d82c02
4 changed files with 58 additions and 1 deletions
+31
View File
@@ -97,6 +97,37 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt
- [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-
password` aufrufen und das eigene Passwort ohne Kenntnis des
alten ersetzen. Der OTP-Flow-Endpoint hatte den Check
`portalPasswordMustChange === true` nicht.
- **Konsequenz**: Bei XSS oder kurzlebigem Token-Diebstahl konnte
ein Angreifer das Passwort dauerhaft übernehmen.
- **Fix**: Eine Zeile in `auth.controller.ts`
`prisma.customer.findUnique` auf `portalPasswordMustChange`,
bei `false` → 403 "Nicht erlaubt".
- **Live-verifiziert**: ohne Flag → 403; mit Flag (nach
send-credentials) → 200, danach Flag automatisch zurück auf
`false` → erneuter Aufruf → 403.
- [x] **Pentest Runde 5 NIEDRIG: consentHash + Public-Grant-Response**
- `consentHash` wurde über `GET /api/customers/:id` zurückgegeben.
Der Hash ist Pseudo-Credential für den öffentlichen Consent-Link
(wer ihn hat, sieht Customer-Name + Kundennummer ohne Auth und
kann Einwilligungen erteilen). **Fix**: in
`SENSITIVE_CUSTOMER_FIELDS` aufgenommen. Wer ihn legitim braucht,
holt ihn über `/gdpr/customer/:id/consent-status` (eigener Check).
- `POST /api/public/consent/:hash/grant` gab den vollen
`CustomerConsent[]`-Array inkl. IP-Adressen und `createdBy`
(Kunden-Name) zurück. **Fix**: Response auf
`{ granted: <count> }` reduziert. Frontend nutzt eh nur
`success`-Flag.
- **Live-verifiziert**: `consentHash: null` in customer-Response,
`consentHash` weiterhin in `/gdpr/.../consent-status`,
Grant-Response liefert nur `{granted: 4}` ohne Extra-Keys.
- [x] **🚨 Pentest Runde 4 HOCH: Cockpit-IDOR (Portal-User sah ALLE Kunden)**
- **Realer Angriff**: Portal-User Max bekam mit seinem Token
`GET /api/contracts/cockpit` → komplette Vertragsliste ALLER