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 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 14:45:10 +02:00
co-authored by Claude Opus 5
parent ac62198a01
commit 2d55fd23f9
3 changed files with 73 additions and 10 deletions
@@ -176,6 +176,10 @@ export async function verifyIntegrity(req: AuthRequest, res: Response) {
(!result.backlogTampered.length && !result.backlogMissing.length (!result.backlogTampered.length && !result.backlogMissing.length
? ' Der Blattbestand des Siegels stimmt nicht mehr mit der signierten Wurzel überein.' ? ' Der Blattbestand des Siegels stimmt nicht mehr mit der signierten Wurzel überein.'
: '') : '')
: 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' : result.backlogSealStatus === 'kein_siegel'
? ' Hinweis: Der Altbestand ist nicht versiegelt Änderungen daran wären nicht erkennbar.' ? ' Hinweis: Der Altbestand ist nicht versiegelt Änderungen daran wären nicht erkennbar.'
: ''; : '';
@@ -188,8 +192,15 @@ export async function verifyIntegrity(req: AuthRequest, res: Response) {
'Jeder Marker enthält den Befund vor dem Siegeln; bei Zweifeln dort vergleichen.' '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 const message = tampered > 0
? `${tampered} MANIPULIERTE Einträge gefunden` + (gaps > 0 ? ` (zusätzlich ${luecken})` : '') ? `${tampered} MANIPULIERTE Einträge gefunden` + (gaps > 0 ? ` (zusätzlich ${luecken})` : '')
: siegelProblem
? 'Die Kette selbst ist rechnerisch stimmig, ABER:'
: gaps > 0 : gaps > 0
? `Keine Manipulation. ${luecken} Inhalte unverändert.` ? `Keine Manipulation. ${luecken} Inhalte unverändert.`
: 'Alle Einträge sind unverändert und lückenlos verkettet'; : 'Alle Einträge sind unverändert und lückenlos verkettet';
+26 -4
View File
@@ -791,9 +791,10 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
unverifiableEntries: number[]; unverifiableEntries: number[];
/** /**
* Zustand des Bestandssiegels ueber den nicht signierbaren Altbestand. * 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. */ /** Altbestands-Zeilen, deren Inhalt vom Siegel abweicht. */
backlogTampered: number[]; backlogTampered: number[];
/** Gesiegelte Zeilen, die nicht mehr existieren Beweismaterial entfernt. */ /** 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). // ueber denselben Weg faelschbar wie zuvor die Manifeste (R171-01).
const backlogTampered: number[] = []; const backlogTampered: number[] = [];
const backlogMissing: 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; let backlogSealRoot: string | null = null;
const siegelKandidaten = await prisma.auditLog.findMany({ const siegelKandidaten = await prisma.auditLog.findMany({
@@ -971,6 +972,21 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
const siegel = gueltigeSiegel[0]; const siegel = gueltigeSiegel[0];
const backlogSealCount = gueltigeSiegel.length; 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) { if (siegel) {
try { try {
const meta = JSON.parse(siegel.changesAfter || '{}'); 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). // ohne Entdopplung zaehlte dieselbe Zeile zweimal (Pentest R171-03).
const invalidEntries = [...new Set([...tamperedEntries, ...chainGaps])].sort((a, b) => a - b); 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 { return {
valid: invalidEntries.length === 0, valid: invalidEntries.length === 0 && siegelInOrdnung,
checkedCount: logs.length, checkedCount: logs.length,
invalidEntries, invalidEntries,
tamperedEntries, tamperedEntries,
+30
View File
@@ -97,6 +97,36 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt ## ✅ 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) - [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 - **R173-01 (HIGH) Siegel war ueber HTTP tot.** Die generische
`auditMiddleware` protokolliert den `POST /seal-backlog`-Request SELBST als `auditMiddleware` protokolliert den `POST /seal-backlog`-Request SELBST als