Audit-Haerten: Fork, Feldabdeckung, Refresh-Rauschen, Route (Pentest R166)
R166-01 (HIGH): Der GET_LOCK-Ansatz gab die Sperre im finally INNERHALB des Transaktions-Callbacks frei, also vor dem COMMIT. Im Fenster Release-Commit las der naechste Schreiber ein noch nicht sichtbares Kettenende - zwei Zeilen hingen am selben Vorgaenger. Meine vorherige Messung war zu schwach: sie suchte Luecken zwischen Nachbarn, nicht Forks. Fix: einzeiliger Mutex AuditChainLock mit FOR UPDATE (InnoDB-Zeilensperren fallen erst beim COMMIT) plus isolationLevel ReadCommitted. Belegt im Direktvergleich mit geweitetem Fenster: Release-vor-Commit forkt, Zeilensperre nicht. R166-02 (MEDIUM): Der Hash deckte nur 7 Felder ab. changesBefore/After, success, ipAddress, resourceLabel, dataSubjectId, userId/customerId waren ungeschuetzt - ein Einzeledit dort blieb unsichtbar. Fix: hashVersion + generateHashV2 ueber alle Inhaltsspalten. Bestandszeilen behalten Version 1 und bleiben ohne Rehash gueltig. Verifiziert: 5/5 zuvor ungeschuetzte Felder werden jetzt erkannt. R166-03 (LOW): "kein Cookie" (normaler Erstbesuch) wurde als HIGH/abgelehnt gefuehrt - jetzt eigener Ausgang mit LOW. Nur echte Ablehnung bleibt HIGH. R166-04 (LOW, pre-existing): GET /retention-policies wurde von GET /:id verschluckt. Konkrete Routen jetzt vor der Parameter-Route. Design-Empfehlungen: runRetentionCleanup schreibt ein Loeschungs-Manifest (ID-Bereich, Anzahl, Policy, Cutoff) als eigenen verketteten Eintrag - Luecken ausserhalb bleiben erklaerungsbeduerftig. rehashAll schreibt einen Marker. Verifiziert: 50 parallele Schreiber -> 50/50, 0 Forks, alle V2, manipuliert 0, Luecken unveraendert 7. tsc + vite build gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,22 @@ const ACTION_LABELS: Record<string, string> = {
|
||||
TOKEN_REFRESH: 'Sitzung verlängert',
|
||||
};
|
||||
|
||||
/**
|
||||
* Unterscheidet die drei Ausgaenge von POST /auth/refresh (Pentest R166-03).
|
||||
*
|
||||
* "Kein Cookie vorhanden" ist KEIN Ablehnungsfall: Der Frontend-Interceptor
|
||||
* ruft /refresh beim App-Start auch dann, wenn nie ein Token gesetzt war
|
||||
* (normaler Erstbesuch). Das als HIGH/"abgelehnt" zu fuehren erzeugt genau das
|
||||
* Rauschen, das mit der Entrauschung beseitigt werden sollte.
|
||||
*/
|
||||
function refreshOutcome(responseBody: unknown, success: boolean): 'ok' | 'no-token' | 'rejected' {
|
||||
if (success) return 'ok';
|
||||
const err = responseBody && typeof responseBody === 'object' && 'error' in responseBody
|
||||
? String((responseBody as { error?: unknown }).error ?? '')
|
||||
: '';
|
||||
return /kein refresh-token/i.test(err) ? 'no-token' : 'rejected';
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt ein menschenlesbares Label für den Audit-Log-Eintrag
|
||||
*/
|
||||
@@ -209,8 +225,12 @@ function generateHumanLabel(
|
||||
}
|
||||
if (path.includes('/auth/logout')) return 'Benutzer hat sich abgemeldet';
|
||||
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)';
|
||||
const success = !(responseBody && typeof responseBody === 'object' && (responseBody as { success?: boolean }).success === false);
|
||||
switch (refreshOutcome(responseBody, success)) {
|
||||
case 'ok': return 'Sitzung verlängert (Token erneuert)';
|
||||
case 'no-token': return 'Token-Refresh ohne vorliegenden Token (kein Cookie)';
|
||||
default: return 'Token-Refresh abgelehnt (ungültig/abgelaufen)';
|
||||
}
|
||||
}
|
||||
|
||||
// Kunden-Operationen
|
||||
@@ -460,10 +480,13 @@ export function auditMiddleware(req: AuthRequest, res: Response, next: NextFunct
|
||||
isCustomerPortal: req.user?.isCustomerPortal,
|
||||
action,
|
||||
// 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).
|
||||
// Abgelehnter Refresh (Replay/Brute-Force-Verdacht) → HIGH, damit er in der
|
||||
// Triage nicht neben legitimen Refreshes untergeht (Pentest R164-01).
|
||||
// "Kein Cookie vorhanden" ist dagegen der normale Erstbesuch → LOW (R166-03).
|
||||
// Andere Auth-Events behalten ihre Default-Sensitivität (Authentication → CRITICAL).
|
||||
sensitivity: action === 'TOKEN_REFRESH' ? (responseSuccess ? 'LOW' : 'HIGH') : undefined,
|
||||
sensitivity: action === 'TOKEN_REFRESH'
|
||||
? (refreshOutcome(responseBody, responseSuccess) === 'rejected' ? 'HIGH' : 'LOW')
|
||||
: undefined,
|
||||
resourceType: mapping.type,
|
||||
resourceId,
|
||||
resourceLabel,
|
||||
|
||||
Reference in New Issue
Block a user