Security-Hardening Runde 12: Information-Disclosure + Input-Validation

Pentest Runde 7 (Anschlussrunde):

MEDIUM – Interne Felder in Portal-Responses:
- sanitizeCustomerStrict strippt zusätzlich portalTokenInvalidatedAt,
  portalLastLogin, portalPasswordMustChange, lastBirthdayGreetingYear,
  privacyPolicyPath, businessRegistrationPath, commercialRegisterPath.
- Neue sanitizeContract/Strict + sanitizeContracts/Strict: entfernt
  portalPasswordEncrypted immer (nur über /password-Endpoint mit Audit
  abrufbar), für Portal-User zusätzlich commission/notes/nextReviewDate.
- getContract + getContracts wählen je nach isCustomerPortal die
  passende Variante. Mitarbeiter sehen commission/notes weiterhin.

LOW – Integer-Truncation bei IDs:
parseInt('6abc') → 6 lief vorher durch. Neue Heuristik-Middleware
unter /api: jedes Pfad-Segment, das mit Ziffer beginnt aber nicht
aus reinen Ziffern besteht, wird mit 400 abgelehnt. Trifft alle
Sub-Router ohne dass jede Route einzeln angefasst werden muss.

INFO – Rate-Limit: Code-Stand limit=10 für Login, limit=5 für
Password-Reset (lokal verifiziert: 11. failed login = 429). Pentester
sah vermutlich noch älteren Build. Kein Code-Change.

Live-verifiziert:
- /customers/6abc → 400 "Ungültige ID im URL-Pfad"
- /customers/3 → 200, /contracts/1abc/history → 400, normale Pfade OK
- Portal-User /customers/3: keine portalLastLogin/portalPasswordMustChange/
  portalTokenInvalidatedAt/etc. mehr in Response
- Portal-User /contracts/15: keine commission/notes/portalPasswordEncrypted/
  nextReviewDate
- Admin /contracts/15: commission/notes/nextReviewDate sichtbar,
  portalPasswordEncrypted weg

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 08:51:52 +02:00
parent c744eebfa3
commit 28c91759df
4 changed files with 147 additions and 4 deletions
+35
View File
@@ -97,6 +97,41 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt
- [x] **🚨 Pentest Runde 7 (Anschlussrunde) Information-Disclosure + Input-Validation**
- **MEDIUM Interne Felder in Portal-Responses**:
* `sanitizeCustomerStrict` strippt jetzt zusätzlich
`portalTokenInvalidatedAt`, `portalLastLogin`,
`portalPasswordMustChange`, `lastBirthdayGreetingYear`,
`privacyPolicyPath`, `businessRegistrationPath`,
`commercialRegisterPath`.
* Neue `sanitizeContract` / `sanitizeContractStrict` /
`sanitizeContracts(Strict)`: entfernt
`portalPasswordEncrypted` (immer; ist nur über den dedizierten
`/password`-Endpoint mit Audit-Log abrufbar) und für Portal-
User zusätzlich `commission`, `notes`, `nextReviewDate`.
* `getContract` + `getContracts` rufen jetzt die passende
Sanitize-Variante je nach `req.user.isCustomerPortal` auf;
Mitarbeiter sehen weiterhin commission/notes (Admin-Workflow),
nur `portalPasswordEncrypted` ist generell entfernt (Klartext
nur über dedicated Endpoint).
* Live-verifiziert: Portal sieht 0 Leaks, Admin sieht
commission/notes weiterhin.
- **LOW Integer-Truncation bei IDs**:
`parseInt('6abc')``6` hat alle Endpoints durchgewunken.
Neuer middleware in `index.ts`: jedes URL-Pfad-Segment unter
`/api`, das mit Ziffer beginnt aber nicht aus reinen Ziffern
besteht, wird mit HTTP 400 abgelehnt. Heuristik trifft alle
`/resource/<id>(\D+)`-Patterns ohne dass jeder einzelne
Sub-Router angefasst werden muss.
* Live-verifiziert: `/customers/6abc` → 400 mit klarer Meldung,
`/customers/3` weiterhin 200, `/contracts/1abc/history`
→ 400, normaler Pfade `/audit-logs/customer/3` → 200.
- **INFO Login-Rate-Limit „nach 6 nicht aktiv"**:
Code-Stand `limit: 10` für `loginRateLimiter`, lokal verifiziert:
11. Versuch = 429. Pentester sah vermutlich noch alten Build
oder eine andere Lokation (PW-Reset hat `limit: 5`). Kein
Code-Change.
- [x] **🛠 Rate-Limit-Sperren: Admin-UI zum Freigeben**
- Bei einer Pentest-Runde hat der Tester sich selbst durch zu viele
Login-Versuche ausgesperrt → ohne Container-Restart kein Weg zurück.