48.3 (Rate-Limit fehlt): POST /api/users/:id/password verlangt seit
47.3 die Eingabe des eigenen Admin-Passworts. Ohne Throttle könnte
ein Angreifer mit gestohlenem JWT die Re-Auth per Brute-Force
aushebeln.
- Neuer staffPasswordReAuthLimiter (5 Versuche / 10 min,
bucket: IP + target-user-id, skipSuccessfulRequests: true)
- emit SecurityEvent RATE_LIMIT_HIT severity HIGH
- Vor authenticate gemounted, damit auch unauth-Spamming
begrenzt wird
48.4 (Alter Token überlebt Self-Reset): Nach erfolgreichem Setzen
wird tokenInvalidatedAt des Ziel-Users auf jetzt gesetzt. Greift
besonders bei Self-Reset (Admin setzt sich selbst zurück) – ein
zuvor gestohlenes Token wird sofort ungültig, statt bis zum
natürlichen Ablauf (15 min) brauchbar zu bleiben. Die bestehende
Auth-Middleware liest tokenInvalidatedAt bereits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
28.1 URI-Schema unvollstaendig:
DANGEROUS_URI_SCHEMES erweitert um file:/ftp: – "ftp://evil.com/x.js"
und "file:///etc/passwd" wurden vorher in companyName akzeptiert.
28.2 HTML-Entity-Decoding-Bypass:
stripHtml() lief direkt ueber den Roh-String, "javascript:",
"<script>" und "<script>" umgingen die Regex.
decodeHtmlEntities() dekodiert jetzt numerische (decimal+hex) +
gaengige named entities VOR dem Tag-/URI-Strip.
28.3 Vollmacht-Upload Magic-Byte-Check:
multer pruefte nur client-MIME, HTML/PHP/Shell-Scripts kamen als
application/pdf durch. uploadAuthorizationDocument liest jetzt die
ersten 5 Bytes und verlangt "%PDF-", sonst Loeschen + 400.
28.4 Rate-Limit auf /api/public/consent:
30 Requests pro IP pro 15min. Brute-Force-sicher war der 128-bit-
UUID-Hash schon, aber ohne Limit konnte ein Angreifer das System
mit Audit-Log- und Mail-Spam belasten.
Live-verifiziert auf dev: alle vier Bypaesse blockiert, legitime
Eingaben unangetastet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Die drei letzten wichtigen Features für ein produktionsreifes 1.0.0:
## 1. Passwort vergessen-Flow
Der klassische Selfservice-Reset per Email – sowohl für Mitarbeiter als auch
für Portal-Kunden. User können sich nicht mehr aussperren, Admin muss nicht
mehr manuell eingreifen.
- Neues Link "Passwort vergessen?" auf Login-Seite
- PasswordResetRequest: Email + Typ-Auswahl (Mitarbeiter / Portal)
- PasswordResetConfirm: Token-basierte Bestätigung + neues Passwort (min 6 Zeichen)
- Token ist 2 Stunden gültig, dann muss neu angefordert werden
- Token ist kryptografisch sicher (crypto.randomBytes(32))
- User-Enumeration-Schutz: Backend gibt immer 200 zurück, egal ob Email existiert
- Nach erfolgreichem Reset werden ALLE bestehenden Sessions gekickt
(tokenInvalidatedAt gesetzt) – falls jemand parallel eingeloggt war
DB:
- User.passwordResetToken + passwordResetExpiresAt
- Customer.portalPasswordResetToken + portalPasswordResetExpiresAt
## 2. Rate-Limiting gegen Brute-Force
Mit express-rate-limit:
- Login: 10 Versuche pro 15 Minuten pro IP. Erfolgreiche zählen nicht mit.
- Passwort-Reset-Request: 5 Versuche pro Stunde pro IP (Mail-Flut verhindern)
Sowohl Mitarbeiter-Login als auch Portal-Login geschützt.
## 3. Auto-Geburtstagsgrüße per Cron
Das autoBirthdayGreeting-Flag hatten wir schon, aber kein Scheduler der
ihn wirklich abschickt. Jetzt:
- Läuft täglich um 08:00 Uhr
- Findet Kunden mit heutigem Geburtstag + autoBirthdayGreeting=true
- Nur Email-Kanal (Messenger brauchen Browser-Klick)
- Catch-up 30s nach Server-Start: wenn Server am Geburtstag down war, wird
beim nächsten Boot nachgeholt
- lastBirthdayGreetingYear verhindert Doppelversand
Dependencies: node-cron, @types/node-cron, express-rate-limit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>