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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user