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:
@@ -0,0 +1,15 @@
|
||||
-- Einzeiliger Mutex fuer die Audit-Hash-Kette (Pentest R166-01).
|
||||
--
|
||||
-- Vorher wurde ueber GET_LOCK serialisiert. Dessen RELEASE_LOCK muss auf
|
||||
-- derselben Verbindung laufen und stand daher im finally INNERHALB des
|
||||
-- Transaktions-Callbacks - also VOR dem COMMIT. In diesem Fenster konnte der
|
||||
-- naechste Schreiber die Sperre holen und das Kettenende lesen, bevor die
|
||||
-- Vorgaengerzeile committed war: beide haengten sich an denselben Vorgaenger
|
||||
-- (Fork). InnoDB-Zeilensperren fallen dagegen erst beim COMMIT.
|
||||
CREATE TABLE IF NOT EXISTS `AuditChainLock` (
|
||||
`id` INT NOT NULL,
|
||||
`updatedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT IGNORE INTO `AuditChainLock` (`id`, `updatedAt`) VALUES (1, NOW(3));
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Hash-Versionierung fuer Audit-Eintraege (Pentest R166-02).
|
||||
--
|
||||
-- Der bisherige Hash deckte nur 7 Felder ab (userEmail, action, resourceType,
|
||||
-- resourceId, endpoint, createdAt, previousHash). NICHT gehasht waren u. a.
|
||||
-- changesBefore/changesAfter (die eigentliche Nutzlast), success, ipAddress,
|
||||
-- resourceLabel, dataSubjectId, userId/customerId - ein nachtraeglicher
|
||||
-- Einzeledit an genau diesen Feldern blieb also unsichtbar.
|
||||
--
|
||||
-- Version 2 hasht alle Inhaltsspalten. Bestandszeilen behalten Version 1 und
|
||||
-- werden weiterhin mit dem alten Verfahren geprueft - kein Rehash noetig,
|
||||
-- die Beweiskraft der Vergangenheit bleibt erhalten.
|
||||
ALTER TABLE `AuditLog`
|
||||
ADD COLUMN IF NOT EXISTS `hashVersion` INT NOT NULL DEFAULT 1;
|
||||
@@ -1221,6 +1221,24 @@ model CarInsuranceDetails {
|
||||
|
||||
// ==================== AUDIT LOGGING (DSGVO) ====================
|
||||
|
||||
/// Einzeiliger Mutex fuer die Audit-Hash-Kette (genau eine Zeile, id = 1).
|
||||
///
|
||||
/// Warum eine eigene Tabelle statt GET_LOCK oder `SELECT … FOR UPDATE` auf
|
||||
/// AuditLog selbst:
|
||||
/// - GET_LOCK muss auf derselben Verbindung freigegeben werden. Innerhalb des
|
||||
/// Prisma-Transaktions-Callbacks faellt das Release damit VOR den COMMIT –
|
||||
/// der naechste Schreiber liest das Kettenende, bevor die Vorgaengerzeile
|
||||
/// sichtbar ist, und haengt sich an denselben Vorgaenger (Fork, R166-01).
|
||||
/// - `FOR UPDATE` auf das Kettenende von AuditLog nimmt Gap-/Next-Key-Locks,
|
||||
/// die mit den gleichzeitigen INSERTs kollidieren (Deadlocks, dabei gingen
|
||||
/// 38 von 40 Eintraegen verloren).
|
||||
/// InnoDB-Zeilensperren werden erst beim COMMIT freigegeben – genau das
|
||||
/// schliesst das Fenster.
|
||||
model AuditChainLock {
|
||||
id Int @id
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
enum AuditAction {
|
||||
CREATE
|
||||
READ
|
||||
@@ -1284,6 +1302,9 @@ model AuditLog {
|
||||
createdAt DateTime @default(now())
|
||||
hash String? // SHA-256 Hash des Eintrags
|
||||
previousHash String? // Hash des vorherigen Eintrags
|
||||
/// 1 = Alt-Hash ueber 7 Felder, 2 = Hash ueber alle Inhaltsspalten
|
||||
/// (Pentest R166-02). Bestandszeilen bleiben mit Version 1 gueltig.
|
||||
hashVersion Int @default(1)
|
||||
|
||||
@@index([userId])
|
||||
@@index([customerId])
|
||||
|
||||
Reference in New Issue
Block a user