Rate-Limiting: IPv6-Bypass-Härtung via ipKeyGenerator

loginRateLimiter und staffPasswordReAuthLimiter keyten auf die volle
req.ip. Bei IPv6 kann ein Angreifer aus seinem zugeteilten Block
(/56–/64) pro Request eine neue Adresse nehmen und so das Per-IP-Limit
umgehen – betrifft Login-Bruteforce-Schutz und den Passwort-Set-
Reauth-Limiter.

Fix: req.ip in beiden keyGenerator durch ipKeyGenerator() ersetzt
(express-rate-limit v7). IPv6 wird auf das Subnetz normalisiert
(Library-Default /56), IPv4 bleibt unverändert. Verifiziert: zwei
verschiedene IPv6 im selben /56 ergeben denselben Key.

Die Limiter ohne eigenen keyGenerator (Passwort-Reset, Consent)
normalisieren IPv6 bereits über den Library-Default – damit jetzt
konsistent. Ersetzt den verworfenen aria-WIP, neu auf aktuellem
Stand gebaut.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 16:38:54 +02:00
co-authored by Claude Opus 4.7
parent 627cae9b3b
commit b4dfede494
2 changed files with 24 additions and 3 deletions
+10 -3
View File
@@ -6,7 +6,7 @@
* 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 rateLimit, { ipKeyGenerator } from 'express-rate-limit';
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js';
function onLimitReached(label: string, severity: 'MEDIUM' | 'HIGH') {
@@ -54,7 +54,13 @@ export const loginRateLimiter = rateLimit({
skipSuccessfulRequests: true,
keyGenerator: (req): string => {
const email = (req.body?.email || '').toString().trim().toLowerCase();
const ip = req.ip || 'unknown';
// IPv6-Härtung: ipKeyGenerator normalisiert IPv6 auf das Subnetz
// (Default /56), damit ein Angreifer nicht durch Rotation innerhalb
// seines zugeteilten IPv6-Blocks das Per-IP-Limit umgeht. IPv4 bleibt
// unverändert. Die Limiter OHNE eigenen keyGenerator (Passwort-Reset,
// Consent) machen das schon über den Library-Default hier holen wir
// die Custom-keyGenerator-Limiter auf denselben Stand.
const ip = ipKeyGenerator(req.ip || 'unknown');
return email ? `${ip}|${email}` : `${ip}|<no-email>`;
},
handler: (req, res, _next, options) => {
@@ -107,7 +113,8 @@ export const staffPasswordReAuthLimiter = rateLimit({
},
skipSuccessfulRequests: true,
keyGenerator: (req): string => {
const ip = req.ip || 'unknown';
// IPv6-Härtung wie beim Login-Limiter (siehe dort).
const ip = ipKeyGenerator(req.ip || 'unknown');
const targetUserId = (req.params?.id ?? '<missing>').toString();
return `${ip}|staff-pw|${targetUserId}`;
},