Login-Rate-Limit pro (IP + Email)-Tupel + PUT /portal verbietet password
Login-Rate-Limit:
Bucket-Key jetzt `${ip}|${email-lowercase}`, ein Limiter (10/15min).
Vorher IP-only oder Email-only führten beide zu Problemen:
- IP-only: Proxy-Wechsel umgeht Sperre auf Account-Ebene
- Email-only: Familie hinter NAT (Max vertippt sich → Nina blockiert),
Account-Lockout-DoS möglich
- Tupel: Max gesperrt, Nina von gleicher IP weiterhin frei, Max von
anderer IP auch noch, eigener Account bleibt erreichbar.
Implementation:
- middleware/rateLimit.ts: keyGenerator → ip|email
- routes/auth.routes.ts: nur ein loginRateLimiter am /login + /customer-login
- controllers/rateLimitAdmin.controller.ts: Listing als (IP, Email)-
Tupel, Reset nimmt ipAddress + optional email. Audit-resourceId =
ip|email (gleich wie Bucket-Key) → Listing kann Reset herausfiltern.
- frontend/RateLimits.tsx: Tabelle mit IP- und Account-Spalte,
Reset-Button schickt beides.
PUT /customers/:id/portal:
Body-Felder password/portalPassword/portalPasswordHash/
portalPasswordEncrypted werden explizit mit 400 abgelehnt. Vorher
wurden sie silent ignoriert + HTTP 200, was den Client glauben ließ,
das PW sei gesetzt. Hinweis im Error-Body zeigt auf den dedizierten
POST /portal/password-Endpoint.
Live-verifiziert:
- 11x falsch max@x.de → 429
- Nina/Admin von gleicher IP → durch
- Reset (IP, max) → max wieder 401 statt 429
- PUT /portal {password:"abcd"} → 400 "Felder nicht erlaubt"
- PUT /portal ohne password → 200
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,36 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
||||
|
||||
## ✅ Erledigt
|
||||
|
||||
- [x] **🛡️ Login-Rate-Limit jetzt pro (IP + Email)-Tupel**
|
||||
- Vorher reine IP-basierte Sperre, was zwei Schwächen hatte:
|
||||
a) Familie hinter NAT: Max vertippt sich → Nina kommt nicht rein
|
||||
b) Angreifer wechselt Proxy → wieder 10 freie Versuche pro
|
||||
Account, dieselbe IP-only-Sperre umgangen.
|
||||
- Eine reine Email-Sperre wurde verworfen wegen Account-Lockout-
|
||||
DoS (jeder kann fremde Accounts sperren) + denselben Shared-IP-
|
||||
Problem.
|
||||
- **Lösung**: Bucket-Key ist `${ip}|${email-lowercase}`. Damit:
|
||||
* Max von IP-A 10x vergeigt → (IP-A, max) gesperrt
|
||||
* Nina von IP-A → eigenes Bucket (IP-A, nina), unbetroffen
|
||||
* Admin von IP-A mit richtigem PW → erfolgreicher Login
|
||||
* Max von IP-B → eigenes Bucket (IP-B, max), darf wieder
|
||||
- Implementation: `loginRateLimiter.keyGenerator = ${ip}|${email}`
|
||||
in `middleware/rateLimit.ts`; nur ein Limiter, kein zusätzlicher
|
||||
Email-only.
|
||||
- Admin-UI: Listing zeigt Tupel (IP, Email), Reset schickt
|
||||
beides mit, Audit-Log resourceId = `${ip}|${email}`.
|
||||
- **Live-verifiziert** (4 Schritte):
|
||||
11x falsch max → 429, Nina/Admin von gleicher IP → durch,
|
||||
max bleibt gesperrt, Reset → max wieder 401.
|
||||
|
||||
- [x] **🚨 PUT /customers/:id/portal mit `password` im Body → 400**
|
||||
- Endpoint nahm `password` silent entgegen, ignorierte es, gab
|
||||
aber HTTP 200 zurück → Client glaubte fälschlich, das Passwort
|
||||
sei gesetzt. Fix: explizite Body-Validierung – `password`,
|
||||
`portalPassword`, `portalPasswordHash`, `portalPasswordEncrypted`
|
||||
sind verbotene Felder, HTTP 400 mit Hinweis auf den dedizierten
|
||||
`POST /portal/password`-Endpoint.
|
||||
|
||||
- [x] **🚨 Pentest Runde 17 – JWT-TTL + Pentest-Marker-Detection**
|
||||
- **21.1 Access-Token 7 Tage**: Bug-Quelle waren die `.env`-Files,
|
||||
die noch die alte Konvention vor der Refresh-Token-Trennung
|
||||
|
||||
Reference in New Issue
Block a user