Refresh-Fehlschlag: Detection-Gap geschlossen (Pentest R164-01)

Fehlgeschlagener /auth/refresh (Replay/Brute-Force auf geraubte
Refresh-Tokens) wurde als TOKEN_REFRESH/LOW geloggt und entging der
Alarmierung - ein Angreifer konnte von /login auf /refresh ausweichen,
um unter der LOGIN_FAILED-Schwelle zu bleiben.

Audit-Actions speisen die Alert-Engine nicht (die zaehlt SecurityEvent
via emit). Fix daher an zwei Ebenen:

- Detection: refresh()-Catch emittiert TOKEN_REJECTED -> greift die
  bestehende Schwelle (>=3 TOKEN_REJECTED/5min/IP -> CRITICAL). Severity
  wie Access-Token: abgelaufen/revoked = LOW (kein Sofort-Alert),
  ungueltige Signatur/Manipulation = HIGH. auth.service reicht dafuer
  err.code REFRESH_EXPIRED/REFRESH_INVALID durch. "Kein Cookie" emittiert
  bewusst nicht (normaler Erstbesuch).
- Audit-Triage: fehlgeschlagener Refresh -> Sensitivitaet HIGH statt LOW
  + Label "Token-Refresh abgelehnt". Action bleibt TOKEN_REFRESH
  (semantisch ein Refresh, kein Login).

Verifiziert: tsx-Test abgelaufen->LOW, manipuliert/garbage->HIGH; tsc gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 18:20:21 +02:00
co-authored by Claude Opus 4.8
parent de0d6bd817
commit d599eb3702
4 changed files with 59 additions and 7 deletions
+12 -4
View File
@@ -49,7 +49,10 @@ function determineAction(method: string, path: string, success: boolean): AuditA
if (path.includes('/auth/logout')) {
return 'LOGOUT';
}
// Stiller Token-Refresh (Cookie) kein interaktiver Login, eigene Action
// Stiller Token-Refresh (Cookie) kein interaktiver Login, eigene Action.
// Erfolg vs. Fehlschlag wird NICHT über die Action getrennt (semantisch beides
// ein Refresh), sondern downstream über success-Flag + Sensitivität (LOW/HIGH)
// und den TOKEN_REJECTED-SecurityEvent im Controller (Pentest R164-01).
if (path.includes('/auth/refresh')) {
return 'TOKEN_REFRESH';
}
@@ -203,7 +206,10 @@ function generateHumanLabel(
: `Anmeldung fehlgeschlagen für ${email}`;
}
if (path.includes('/auth/logout')) return 'Benutzer hat sich abgemeldet';
if (path.includes('/auth/refresh')) return 'Sitzung verlängert (Token erneuert)';
if (path.includes('/auth/refresh')) {
const failed = responseBody && typeof responseBody === 'object' && (responseBody as { success?: boolean }).success === false;
return failed ? 'Token-Refresh abgelehnt (ungültig/abgelaufen)' : 'Sitzung verlängert (Token erneuert)';
}
// Kunden-Operationen
if (resourceType === 'Customer') {
@@ -442,9 +448,11 @@ export function auditMiddleware(req: AuthRequest, res: Response, next: NextFunct
customerId: req.user?.customerId,
isCustomerPortal: req.user?.isCustomerPortal,
action,
// Stiller Token-Refresh ist Routine → LOW statt CRITICAL (sonst Log-Flut).
// Erfolgreicher Token-Refresh ist Routine → LOW statt CRITICAL (sonst Log-Flut).
// Fehlgeschlagener Refresh (Replay/Brute-Force-Verdacht) → HIGH, damit er in
// der Triage nicht neben legitimen Refreshes untergeht (Pentest R164-01).
// Andere Auth-Events behalten ihre Default-Sensitivität (Authentication → CRITICAL).
sensitivity: action === 'TOKEN_REFRESH' ? 'LOW' : undefined,
sensitivity: action === 'TOKEN_REFRESH' ? (responseSuccess ? 'LOW' : 'HIGH') : undefined,
resourceType: mapping.type,
resourceId,
resourceLabel,