From 89ae7b73a1c3343a6ad00b091cb6fd211891aac6 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 19 Aug 2026 09:32:22 +0200 Subject: [PATCH] Audit-Integritaet: Dauer-Fehlalarm ueber 67 % des Logs behoben verifyIntegrity meldete 3107 von 4630 Zeilen als "manipuliert" - davon 3100 Fehlalarme, exakt die Zeilen mit resourceId = NULL aus 08.02.-01.05.2026. Ursache: Der R121-Fix nahm an, resourceId sei beim Schreiben immer undefined gewesen (Key faellt bei JSON.stringify weg) und daher wuerden alle Bestands-Hashes ohne Rehash matchen. Das gilt erst ab ~01.05.2026 - aeltere Zeilen wurden mit explizitem null serialisiert, der Key war drin. Sicherheitsrelevant, weil ein staendig grundlos ausloesender Alarm ignoriert wird - echte Manipulation ginge im Laerm unter. Fix: generateHashLegacy() reproduziert das alte Schreibverhalten, verifyIntegrity akzeptiert Altbestand ueber diesen Fallback (nur geprueft, wenn die aktuelle Variante nicht passt). Bewusst KEIN Rehash - der wuerde die Manipulations-Beweiskraft der Vergangenheit zerstoeren. Gespeicherte Hashes bleiben unangetastet. Verifiziert: ungueltig 3107 -> 7 (echte Ketten-Brueche). Adversarial gegengetestet: Manipulation an userEmail/action/endpoint/createdAt/resourceId wird bei alten wie neuen Zeilen zu 100 % erkannt (10/10). tsc gruen. Co-Authored-By: Claude Opus 5 --- backend/src/services/audit.service.ts | 61 +++++++++++++++++++++++++-- docs/todo.md | 23 ++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/backend/src/services/audit.service.ts b/backend/src/services/audit.service.ts index 716d23d2..b893f083 100644 --- a/backend/src/services/audit.service.ts +++ b/backend/src/services/audit.service.ts @@ -119,6 +119,48 @@ function generateHash(data: { return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex'); } +/** + * Historische Hash-Variante (Bestandsdaten ~08.02.–01.05.2026). + * + * Der R121-Fix ging davon aus, dass `resourceId` beim Schreiben immer + * `undefined` war (→ Key faellt bei JSON.stringify weg) und daher ALLE + * Bestands-Hashes ohne Rehash matchen. Das stimmt erst ab ~01.05.2026: + * aeltere Zeilen wurden mit explizitem `null` serialisiert, der Key war also + * DRIN. Ergebnis war ein Dauer-Fehlalarm ueber ~3100 Zeilen (67 % des Logs) – + * und ein Alarm, der staendig grundlos ausloest, verdeckt echte Manipulation. + * + * Diese Funktion reproduziert exakt das alte Schreibverhalten, damit + * verifyIntegrity Altbestand als gueltig erkennt – OHNE Rehash. Ein Rehash + * waere der naheliegende Schnellfix, wuerde die Manipulations-Beweiskraft der + * Vergangenheit aber unwiederbringlich zerstoeren. + * + * Sicherheit: Beide Varianten hashen dieselben Feldwerte, nur die + * Serialisierung des nullish `resourceId` unterscheidet sich. Ein Angreifer + * gewinnt dadurch keinen Spielraum, Inhalte zu aendern – nur die Kodierung + * eines leeren Feldes ist doppelt zulaessig. + */ +function generateHashLegacy(data: { + userEmail: string; + action: AuditAction; + resourceType: string; + resourceId?: string | null; + endpoint: string; + createdAt: Date; + previousHash?: string | null; +}): string { + const payload: Record = { + userEmail: data.userEmail, + action: data.action, + resourceType: data.resourceType, + resourceId: data.resourceId ?? null, + endpoint: data.endpoint, + createdAt: data.createdAt.toISOString(), + previousHash: data.previousHash || '', + }; + + return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex'); +} + /** * Bestimmt die Sensitivität basierend auf dem Ressourcentyp */ @@ -416,10 +458,23 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{ previousHash: log.previousHash, }); - // Prüfen ob Hash übereinstimmt + // Prüfen ob Hash übereinstimmt – Altbestand darf die historische + // Serialisierung nutzen (siehe generateHashLegacy). Legacy wird nur + // geprüft, wenn die aktuelle Variante nicht passt. if (log.hash !== expectedHash) { - invalidEntries.push(log.id); - continue; + const legacyHash = generateHashLegacy({ + userEmail: log.userEmail, + action: log.action, + resourceType: log.resourceType, + resourceId: log.resourceId, + endpoint: log.endpoint, + createdAt: log.createdAt, + previousHash: log.previousHash, + }); + if (log.hash !== legacyHash) { + invalidEntries.push(log.id); + continue; + } } // Prüfen ob previousHash mit dem Hash des vorherigen Eintrags übereinstimmt diff --git a/docs/todo.md b/docs/todo.md index 8b0f381b..d0d6c35c 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,29 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🛡️ Audit-Integritaet: Dauer-Fehlalarm ueber 67 % des Logs behoben** (2026-08-18) + - Beim Nachpruefen aufgefallen: `verifyIntegrity` meldete **3107 von 4630** + Zeilen als „manipuliert“. Davon waren **3100 Fehlalarme** – eingegrenzt auf + exakt die Zeilen mit `resourceId = NULL` aus dem Zeitraum 08.02.–01.05.2026. + - Ursache: Der R121-Fix nahm an, `resourceId` sei beim Schreiben immer + `undefined` gewesen (Key faellt bei `JSON.stringify` weg) und daher wuerden + **alle** Bestands-Hashes ohne Rehash matchen. Das gilt erst ab ~01.05.2026 – + aeltere Zeilen wurden mit explizitem `null` serialisiert, der Key war DRIN. + - Warum das sicherheitsrelevant ist: Ein Alarm, der staendig grundlos + ausloest, wird ignoriert – **echte** Manipulation ginge im Lärm unter + (gleiches Muster wie beim Refresh-Rauschen, [R165]). + - Fix: `generateHashLegacy()` reproduziert das alte Schreibverhalten; + `verifyIntegrity` akzeptiert Altbestand ueber diesen Fallback (nur geprueft, + wenn die aktuelle Variante nicht passt). **Kein Rehash** – der waere der + naheliegende Schnellfix, wuerde die Manipulations-Beweiskraft der + Vergangenheit aber unwiederbringlich zerstoeren. Gespeicherte Hashes + bleiben unangetastet. + - Verifiziert: ungueltige Zeilen **3107 → 7** (die 7 sind echte Ketten-Brueche, + siehe naechster Punkt). Adversarial gegengetestet: Manipulation an + `userEmail`/`action`/`endpoint`/`createdAt`/`resourceId` wird bei ALTEN wie + NEUEN Zeilen weiterhin zu 100 % erkannt (10/10), unveraenderte Zeilen + akzeptiert. `tsc` gruen. + - [x] **🐛 Audit-Log: Pfad-Matching kaputt – Auth-Actions generisch (Pentest R165-01)** (2026-08-18) - Pentester meldete: Entrauschung (`de0d6bd`) live **nicht wirksam** – jeder `/refresh` weiter `CREATE / CRITICAL / „Anmeldung erstellt“`. Zusatzbefund: