Pentest 48.3 MEDIUM + 48.4 INFO: Rate-Limit + Token-Invalidierung beim Staff-Passwort-Reset

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>
This commit is contained in:
2026-06-01 13:01:44 +02:00
parent 2c0166ed99
commit 5d21574c81
3 changed files with 51 additions and 3 deletions
+35
View File
@@ -82,6 +82,41 @@ export const passwordResetRateLimiter = rateLimit({
},
});
/**
* 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 ?? '<missing>').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,