/** * Rate-Limiting-Middleware für sensible Endpoints (Login, Passwort-Reset). * Schützt gegen Brute-Force- und Credential-Stuffing-Angriffe. * * Wenn ein Limit überschritten wird, emit() wir zusätzlich ein * SecurityEvent (RATE_LIMIT_HIT) – damit der Monitoring-View und das * Alert-System sehen, wenn jemand auf die Tür hämmert. */ import rateLimit from 'express-rate-limit'; import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js'; function onLimitReached(label: string, severity: 'MEDIUM' | 'HIGH') { return (req: any, _res: any) => { const ctx = contextFromRequest(req); emitSecurityEvent({ type: 'RATE_LIMIT_HIT', severity, message: `Rate-Limit überschritten: ${label}`, ipAddress: ctx.ipAddress, userEmail: req.body?.email, endpoint: ctx.endpoint, details: { limiter: label }, }); }; } /** * Login-Limiter: 10 Fehlversuche pro 15 min PRO (IP + Email)-Tuple. * * Das Bucket ist gezielt das Paar, nicht IP allein und nicht Email allein: * - IP allein wäre kein Schutz: ein Angreifer wechselt Proxy, hat wieder * 10 freie Versuche gegen den gleichen Account. * - Email allein erzeugt False-Positives (Familie hinter NAT: Max * vertippt sich → Nina kommt von gleicher IP nicht mehr rein) und * macht Account-Lockout-DoS möglich (Angreifer sperrt fremde Accounts * aus, indem er von beliebigen IPs falsche PWs gegen sie probiert). * - Tuple (IP, Email): Max kann sich nicht mehr einloggen, Nina von * gleicher IP schon. Max von einer anderen IP auch, solange er das * richtige PW hat – ihre eigene Spur in den Buckets ist sauber. * * keyGenerator → `${ip}|${email-lowercase}`. Bei fehlender Email * (z.B. komplett leerer Body) Fallback nur auf IP, damit kein * Single-Shared-Bucket entsteht. */ export const loginRateLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 10, standardHeaders: 'draft-7', legacyHeaders: false, message: { success: false, error: 'Zu viele Login-Versuche für diese Kombination aus Account und IP. Bitte in 15 Minuten erneut versuchen.', }, skipSuccessfulRequests: true, keyGenerator: (req): string => { const email = (req.body?.email || '').toString().trim().toLowerCase(); const ip = req.ip || 'unknown'; return email ? `${ip}|${email}` : `${ip}|`; }, handler: (req, res, _next, options) => { onLimitReached('login', 'HIGH')(req, res); res.status(options.statusCode).json(options.message); }, }); /** * Passwort-Reset-Anfrage: 5 Versuche pro Stunde pro IP. * Verhindert Mail-Flut und gezielte Brute-Force über Reset-Links. */ export const passwordResetRateLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 Stunde limit: 5, standardHeaders: 'draft-7', legacyHeaders: false, message: { success: false, error: 'Zu viele Passwort-Reset-Anfragen. Bitte in einer Stunde erneut versuchen.', }, handler: (req, res, _next, options) => { onLimitReached('password-reset', 'MEDIUM')(req, res); res.status(options.statusCode).json(options.message); }, }); /** * Staff-Password-Set-Limiter (Pentest 48.3, 2026-06-01): * POST /api/users/:id/password verlangt seit 47.3 die Eingabe des eigenen * Admin-Passworts (`currentPassword`). Ohne Throttle könnte ein Angreifer * mit gestohlenem JWT die 25-Zeichen-Passwort-Policy zwar nicht erraten, * aber kürzere/typische Admin-Passwörter (z.B. Stagings, kompromittierte * Setups) per Brute-Force durchprobieren – und damit den Re-Auth-Fix * komplett aushebeln. * * Bucket: (IP, target-user-id). Damit walked ein Angreifer pro Opfer * langsam und kann nicht mit einem stolen-token gegen alle Staff-User * parallel anrennen. `skipSuccessfulRequests: true`, weil legitime * Passwort-Resets nicht den Counter füllen sollen. */ export const staffPasswordReAuthLimiter = rateLimit({ windowMs: 10 * 60 * 1000, // 10 Minuten limit: 5, standardHeaders: 'draft-7', legacyHeaders: false, message: { success: false, error: 'Zu viele fehlgeschlagene Passwort-Set-Versuche. Bitte in 10 Minuten erneut versuchen.', }, skipSuccessfulRequests: true, keyGenerator: (req): string => { const ip = req.ip || 'unknown'; const targetUserId = (req.params?.id ?? '').toString(); return `${ip}|staff-pw|${targetUserId}`; }, handler: (req, res, _next, options) => { onLimitReached('staff-password-set', 'HIGH')(req, res); res.status(options.statusCode).json(options.message); }, }); /** * Public-Consent-Endpoints (/api/public/consent/:hash[/grant|/pdf]) sind * unauthenticated. Der hash ist 128-bit-UUID → kein Brute-Force-Risk, * aber DoS-Vektor: ohne Limit könnte ein Angreifer endlos POSTen und * den Service durch Audit-Log-Spam + Mail-Versand belasten. * (Pentest 2026-05-20 INFO 28.4). 30 Requests pro 15 min pro IP reicht * für legitime Kunden weit aus. */ export const publicConsentRateLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 30, standardHeaders: 'draft-7', legacyHeaders: false, message: { success: false, error: 'Zu viele Anfragen. Bitte in 15 Minuten erneut versuchen.', }, handler: (req, res, _next, options) => { onLimitReached('public-consent', 'MEDIUM')(req, res); res.status(options.statusCode).json(options.message); }, });