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 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 09:32:22 +02:00
co-authored by Claude Opus 5
parent ad520e20a7
commit 89ae7b73a1
2 changed files with 81 additions and 3 deletions
+58 -3
View File
@@ -119,6 +119,48 @@ function generateHash(data: {
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex'); 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<string, unknown> = {
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 * Bestimmt die Sensitivität basierend auf dem Ressourcentyp
*/ */
@@ -416,10 +458,23 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
previousHash: log.previousHash, 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) { if (log.hash !== expectedHash) {
invalidEntries.push(log.id); const legacyHash = generateHashLegacy({
continue; 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 // Prüfen ob previousHash mit dem Hash des vorherigen Eintrags übereinstimmt
+23
View File
@@ -97,6 +97,29 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt ## ✅ 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) - [x] **🐛 Audit-Log: Pfad-Matching kaputt Auth-Actions generisch (Pentest R165-01)** (2026-08-18)
- Pentester meldete: Entrauschung (`de0d6bd`) live **nicht wirksam** jeder - Pentester meldete: Entrauschung (`de0d6bd`) live **nicht wirksam** jeder
`/refresh` weiter `CREATE / CRITICAL / „Anmeldung erstellt“`. Zusatzbefund: `/refresh` weiter `CREATE / CRITICAL / „Anmeldung erstellt“`. Zusatzbefund: