From 2d55fd23f9c0b0dd8afa4edb5699261c8ff4c80c Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 21 Aug 2026 14:45:10 +0200 Subject: [PATCH] Siegel-Entfernung wird erkannt (Pentest R174-01, HIGH) Der Siegelzustand hing ausschliesslich am Marker im Audit-Log - und den kann ein DB-Schreiber ohne Schluessel loeschen. Danach meldete die Pruefung kein_siegel, also Entwarnung, ununterscheidbar von "nie versiegelt". Die Blattwerte blieben verwaist liegen und wurden nie konsultiert, die zuvor erkannte V1-Faelschung war wieder unsichtbar. Besonders bitter im Tail-Fall: Steht der Marker am Ketten-Ende - genau der Zustand direkt nach dem einmaligen seal-backlog beim Deploy - reisst beim Loeschen nicht einmal eine Luecke. Ergebnis war valid:true und "Alle Eintraege unveraendert und lueckenlos verkettet", also null Spur. Meine Antwort auf die Frage des Pentesters war damit falsch: die Luecke reisst nur, solange der Marker nicht am Ende steht. Fix: Gegen-Check "Blaetter vorhanden, aber kein gueltiger Marker" -> neuer Status entfernt statt kein_siegel, mit ausdruecklicher Meldung. Ein gebrochenes oder entferntes Siegel kippt jetzt valid auf false, auch ohne beanstandete Einzelzeile. Der beruhigende Einstiegssatz entfaellt bei Siegelproblemen. Verifiziert ueber den echten HTTP-Pfad, exakt Szenario 4: Marker nachweislich das Ketten-Ende, Marker + Middleware-Decoy geloescht, 0 Ketten-Luecken -> valid:false, Status entfernt, Klartext-Warnung. Auch der Nicht-Tail-Fall geprueft. Wegwerf-Datenbanken danach geloescht, Dev unberuehrt. Co-Authored-By: Claude Opus 5 --- .../src/controllers/auditLog.controller.ts | 23 ++++++++++---- backend/src/services/audit.service.ts | 30 ++++++++++++++++--- docs/todo.md | 30 +++++++++++++++++++ 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/backend/src/controllers/auditLog.controller.ts b/backend/src/controllers/auditLog.controller.ts index a2345003..df08c72b 100644 --- a/backend/src/controllers/auditLog.controller.ts +++ b/backend/src/controllers/auditLog.controller.ts @@ -176,9 +176,13 @@ export async function verifyIntegrity(req: AuthRequest, res: Response) { (!result.backlogTampered.length && !result.backlogMissing.length ? ' Der Blattbestand des Siegels stimmt nicht mehr mit der signierten Wurzel überein.' : '') - : result.backlogSealStatus === 'kein_siegel' - ? ' Hinweis: Der Altbestand ist nicht versiegelt – Änderungen daran wären nicht erkennbar.' - : ''; + : result.backlogSealStatus === 'entfernt' + ? ' ⚠ Bestandssiegel ENTFERNT: Es liegen versiegelte Blattwerte vor, aber kein gültiger ' + + 'Siegel-Marker mehr. Der Marker wurde gelöscht oder unbrauchbar gemacht – Änderungen am ' + + 'Altbestand wären dadurch wieder unsichtbar. Das ist KEIN Normalzustand.' + : result.backlogSealStatus === 'kein_siegel' + ? ' Hinweis: Der Altbestand ist nicht versiegelt – Änderungen daran wären nicht erkennbar.' + : ''; // Erneutes Siegeln kann legitim sein, verdient aber einen Blick: es // ersetzt die zuvor beglaubigte Wurzel (Pentest R173-03). @@ -188,11 +192,18 @@ export async function verifyIntegrity(req: AuthRequest, res: Response) { 'Jeder Marker enthält den Befund vor dem Siegeln; bei Zweifeln dort vergleichen.' : ''; + // Bei problematischem Siegel darf KEIN beruhigender Einstiegssatz stehen – + // sonst entwertet er die nachfolgende Warnung (Pentest R174-01). + const siegelProblem = + result.backlogSealStatus === 'gebrochen' || result.backlogSealStatus === 'entfernt'; + const message = tampered > 0 ? `${tampered} MANIPULIERTE Einträge gefunden` + (gaps > 0 ? ` (zusätzlich ${luecken})` : '') - : gaps > 0 - ? `Keine Manipulation. ${luecken} – Inhalte unverändert.` - : 'Alle Einträge sind unverändert und lückenlos verkettet'; + : siegelProblem + ? 'Die Kette selbst ist rechnerisch stimmig, ABER:' + : gaps > 0 + ? `Keine Manipulation. ${luecken} – Inhalte unverändert.` + : 'Alle Einträge sind unverändert und lückenlos verkettet'; res.json({ success: true, diff --git a/backend/src/services/audit.service.ts b/backend/src/services/audit.service.ts index 81af66de..ab229702 100644 --- a/backend/src/services/audit.service.ts +++ b/backend/src/services/audit.service.ts @@ -791,9 +791,10 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{ unverifiableEntries: number[]; /** * Zustand des Bestandssiegels ueber den nicht signierbaren Altbestand. - * `kein_siegel` = nie erstellt (oder Marker nicht pruefbar). + * `kein_siegel` = nie erstellt. `entfernt` = Blaetter vorhanden, aber kein + * gueltiger Marker mehr – der Anker wurde herausgeloest (Pentest R174-01). */ - backlogSealStatus: 'kein_siegel' | 'intakt' | 'gebrochen'; + backlogSealStatus: 'kein_siegel' | 'intakt' | 'gebrochen' | 'entfernt'; /** Altbestands-Zeilen, deren Inhalt vom Siegel abweicht. */ backlogTampered: number[]; /** Gesiegelte Zeilen, die nicht mehr existieren – Beweismaterial entfernt. */ @@ -938,7 +939,7 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{ // ueber denselben Weg faelschbar wie zuvor die Manifeste (R171-01). const backlogTampered: number[] = []; const backlogMissing: number[] = []; - let backlogSealStatus: 'kein_siegel' | 'intakt' | 'gebrochen' = 'kein_siegel'; + let backlogSealStatus: 'kein_siegel' | 'intakt' | 'gebrochen' | 'entfernt' = 'kein_siegel'; let backlogSealRoot: string | null = null; const siegelKandidaten = await prisma.auditLog.findMany({ @@ -971,6 +972,21 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{ const siegel = gueltigeSiegel[0]; const backlogSealCount = gueltigeSiegel.length; + // Gegen-Check (Pentest R174-01): Der Siegelzustand hing ausschliesslich am + // Marker – und den kann ein DB-Schreiber OHNE Schluessel loeschen. Danach + // meldete die Pruefung `kein_siegel`, also Entwarnung, ununterscheidbar von + // „nie versiegelt“. Steht der Marker am Ketten-Ende (genau der Zustand + // direkt nach dem einmaligen Siegeln beim Deploy), riss dabei nicht einmal + // eine Luecke: das Ergebnis war `valid: true` trotz gefaelschter gesiegelter + // Zeile. Die Blaetter blieben verwaist zurueck und wurden nie konsultiert. + // + // Deshalb gilt jetzt: Blaetter vorhanden, aber kein gueltiger Marker = + // Siegel ENTFERNT und damit ein Befund – nicht „nie versiegelt“. + const blattAnzahl = await prisma.auditBacklogSeal.count(); + if (!siegel && blattAnzahl > 0 && siegelSchluessel.length > 0) { + backlogSealStatus = 'entfernt'; + } + if (siegel) { try { const meta = JSON.parse(siegel.changesAfter || '{}'); @@ -1122,8 +1138,14 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{ // ohne Entdopplung zaehlte dieselbe Zeile zweimal (Pentest R171-03). const invalidEntries = [...new Set([...tamperedEntries, ...chainGaps])].sort((a, b) => a - b); + // Ein gebrochenes oder entferntes Siegel muss `valid` kippen, auch wenn keine + // einzelne Zeile beanstandet ist – sonst bliebe der stille Anker-Verlust + // unsichtbar (R174-01). + const siegelInOrdnung = + backlogSealStatus === 'intakt' || backlogSealStatus === 'kein_siegel'; + return { - valid: invalidEntries.length === 0, + valid: invalidEntries.length === 0 && siegelInOrdnung, checkedCount: logs.length, invalidEntries, tamperedEntries, diff --git a/docs/todo.md b/docs/todo.md index 4f8e7d1d..e9f4c375 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,36 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🚨 Siegel-Entfernung wird erkannt (Pentest R174-01, HIGH)** (2026-08-18) + - Fund: Der Siegelzustand hing **ausschliesslich** am Marker im Audit-Log – + und den kann ein DB-Schreiber **ohne Schluessel** loeschen. Danach meldete + die Pruefung `kein_siegel`, also **Entwarnung**, ununterscheidbar von „nie + versiegelt“. Die 11 Blattwerte blieben verwaist liegen und wurden nie + konsultiert; die zuvor erkannte V1-Faelschung war wieder unsichtbar. + - Besonders bitter im **Tail-Fall**: Steht der Marker am Ketten-Ende – genau + der Zustand direkt nach dem einmaligen `seal-backlog` beim Deploy – reisst + beim Loeschen nicht einmal eine Luecke. Ergebnis war `valid: true`, + „Alle Einträge unverändert und lückenlos verkettet“, **null Spur**. Meine + Antwort auf seine Frage (b) war damit falsch: die Luecke reisst nur, + solange der Marker NICHT am Ende steht. + - Fix (sein Vorschlag): Gegen-Check „Blaetter vorhanden, aber kein gueltiger + Marker“ → neuer Status **`entfernt`** statt `kein_siegel`, mit + ausdruecklicher Meldung („Das ist KEIN Normalzustand“). Zusaetzlich kippt + ein gebrochenes ODER entferntes Siegel jetzt `valid` auf `false`, auch wenn + keine einzelne Zeile beanstandet ist – sonst bliebe der stille Anker-Verlust + ohne Wirkung. Und der beruhigende Einstiegssatz entfaellt bei + Siegelproblemen, damit er die Warnung nicht entwertet. + - Verifiziert ueber den echten HTTP-Pfad, exakt sein Szenario 4: Marker war + nachweislich das Ketten-Ende, Marker + Middleware-Decoy geloescht, + **0 Ketten-Luecken** – Ergebnis jetzt `valid: false`, Status `entfernt`, + Klartext-Warnung. Zusaetzlich der Nicht-Tail-Fall geprueft. Zwei + Wegwerf-Datenbanken, beide danach geloescht; Dev unberuehrt. + `tsc` + `vite build` gruen. + - Seine Gegenchecks bestaetigt: Frage (a) Middleware kann keine Marker-/ + Manifest-Signatur mit auswertbarem Inhalt nachbilden; Frage (c) + Re-Insert mit derselben id umgeht nichts, weil das Blatt alle Spalten + deckt. + - [x] **🔧 Bestandssiegel betriebstauglich gemacht (Pentest R173-01 HIGH, R173-02/-03 MEDIUM)** (2026-08-18) - **R173-01 (HIGH) – Siegel war ueber HTTP tot.** Die generische `auditMiddleware` protokolliert den `POST /seal-backlog`-Request SELBST als