Siegelwechsel ist ein Alarm, kein Hinweis (Pentest R185-01/-02)

R185-01 (MEDIUM): Die Flanke, die wir selbst gemeldet hatten, hat der
Tester live bestaetigt. seal-backlog war beim ZWEITEN Aufruf genauso
gegatet wie beim ersten ({"confirm":"SEAL"} -> 200), und das Ereignis
landete nur im Audit-Log, nicht im Alarmkanal. Sein Punkt: der
automatische Rueckhalt des Gegenbuchs haengt an `valid` - und `valid`
ueberlebt ein ersetzendes Siegel per Konstruktion. Angriff: Altzeile per
DB-Zugriff loeschen, neu siegeln, Luecke ist beglaubigt, valid wieder
true. Live reproduziert.

Zwei Schichten, in seiner Reihenfolge:

1. Alarmkanal. Neuer SecurityEventType AUDIT_SEAL_CHANGED (Migration
   20260826120000). Erstes Siegeln HIGH, Ersetzen CRITICAL - geht damit
   ueber sendPendingCriticalAlerts sofort per Mail raus. Die Details
   halten Wurzel vorher/nachher und den vollstaendigen Vorbefund fest.
2. Gate. Steht bereits ein Siegel, verlangt der Endpunkt
   {"confirm":"RESEAL"} statt SEAL, mit einem Text, der sagt, was dabei
   verloren geht. Ein Austausch der Beweisgrundlage soll nicht dasselbe
   Wort haben wie das Einrichten.

Und im Gegenbuch selbst: dort stand fuer den Wurzelwechsel ein
console.warn, waehrend der Rueckgabecode auf 0 blieb - also exakt das
Muster, das wir dem CRM zweimal angekreidet haben (R179, R183-02), im
Werkzeug, das dagegen gebaut wurde. Jetzt exit 2, mit alter und neuer
Wurzel samt Blattzahl; "10 Blaetter -> 9 Blaetter" zeigt die Loeschung
sofort. Auch die Erstsiegelung meldet sich, statt stillschweigend
uebernommen zu werden.

Aufloesbar gemacht: der Alarm bricht ab, BEVOR angehaengt wird - ohne
Bestaetigungsweg haette auch ein legitimes Siegeln fuer immer alarmiert
(R183-03-Falle). Neu ist NOTARY_SEAL_ACK, bewusst nicht "true", sondern
die Wurzel selbst (mind. 16 Zeichen): ein stehen gelassener Wert passt
beim naechsten Wechsel nicht mehr und kann keinen weiteren Austausch
durchwinken.

R185-02 (LOW): GET /api/audit-logs?action=<x> gab ungueltige Enum-Werte
roh an die Spalte -> 500. Zweifach schlecht: fehlende Validierung und
Fehler-Orakel (200 vs 500 verraet die Enum-Mitglieder). Jetzt 400 mit
der erlaubten Menge im Klartext. Mitgenommen: sensitivity, Datumsfelder,
Zahlenfelder, Textlaengen und ein Deckel auf limit (200), ueber den sich
sonst die ganze Tabelle an der Seitenlogik vorbei ziehen liess. Beide
Endpunkte.

Getestet ueber HTTP gegen eine Wegwerf-DB, inkl. echtem Gegenbuch-Lauf
mit SSH-signiertem lokalem Repo. Zusaetzlich nachgeholt, was der Tester
nicht herstellen konnte: vollstaendig unsigniertes Protokoll ->
kein_siegel statt der frueheren falschen Entwarnung nicht_noetig, und
seal-backlog nennt den fehlenden Schluessel als naechsten Schritt.
Gegenrichtung geprueft, R183-03 bleibt behoben.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 09:41:11 +02:00
co-authored by Claude Opus 5
parent 7af6b7591b
commit e81a83ae8f
10 changed files with 365 additions and 28 deletions
+60
View File
@@ -97,6 +97,66 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt
- [x] **🚨 Siegelwechsel ist ein Alarm, kein Hinweis + Filter-Validierung (Pentest R185)** (2026-08-26)
- **R185-01 (MEDIUM)** Die Flanke, die wir dem Tester selbst gemeldet
hatten, hat er live bestätigt: `seal-backlog` war beim **zweiten** Aufruf
genauso gegatet wie beim ersten (`{"confirm":"SEAL"}` → 200), und das
Ereignis landete nur im Audit-Log, nicht im Alarmkanal. Sein Punkt: der
automatische Rückhalt des Gegenbuchs hängt an `valid` und `valid`
überlebt ein ersetzendes Siegel **per Konstruktion**. Angriff: Altzeile
per DB-Zugriff löschen → neu siegeln → Lücke ist beglaubigt, `valid`
wieder `true`. **Live reproduziert.**
- Zwei Schichten, in seiner Reihenfolge:
1. **Alarmkanal.** Neuer `SecurityEventType` `AUDIT_SEAL_CHANGED` (Migration
`20260826120000`). Erstes Siegeln → HIGH, Ersetzen → **CRITICAL** (geht
damit über `sendPendingCriticalAlerts` sofort per Mail raus). Die
Ereignis-Details halten Wurzel vorher/nachher und den vollständigen
Vorbefund fest.
2. **Gate.** Steht bereits ein Siegel, verlangt der Endpunkt
`{"confirm":"RESEAL"}` statt `SEAL` mit einem Text, der sagt, was
dabei verloren geht. Ein Austausch der Beweisgrundlage soll nicht
dasselbe Wort haben wie das Einrichten.
- **Und im Gegenbuch selbst:** Dort stand für den Wurzelwechsel ein
`console.warn`, während der Rückgabecode auf **0** blieb also exakt das
Muster, das wir dem CRM zweimal angekreidet haben (R179/R183-02), im
Werkzeug, das dagegen gebaut wurde. Jetzt **exit 2**, mit alter und neuer
Wurzel samt Blattzahl (`10 Blätter → 9 Blätter` zeigt die Löschung
sofort). Auch die Erstsiegelung meldet sich, statt stillschweigend
übernommen zu werden.
- **Auflösbar gemacht:** Der Alarm bricht ab, *bevor* angehängt wird ohne
Bestätigungsweg hätte auch ein legitimes Siegeln für immer alarmiert
(R183-03-Falle). Neu: `NOTARY_SEAL_ACK`. Bewusst **nicht** `true`, sondern
die **Wurzel selbst** (mind. 16 Zeichen): ein stehen gelassener Wert passt
beim nächsten Wechsel nicht mehr und kann keinen weiteren Austausch
durchwinken der Unterschied zu `NOTARY_GENESIS_ACK`, wo genau diese
Falle dokumentiert werden musste.
- **R185-02 (LOW)** `GET /api/audit-logs?action=<x>`: ungültige Enum-Werte
gingen roh an die Spalte → **500**. Zweifach schlecht: fehlende
Validierung *und* Fehler-Orakel (200 vs. 500 verrät die Enum-Mitglieder).
Jetzt 400 mit der erlaubten Menge im Klartext die steht ohnehin in der
Oberfläche. Gleich mitgenommen: `sensitivity`, Datumsfelder
(`new Date('foo')` → Invalid Date → 500), Zahlenfelder (`parseInt` → NaN),
Textlängen, und ein Deckel auf `limit` (200), über den sich sonst die
ganze Tabelle an der Seitenlogik vorbei ziehen ließ. Beide Endpunkte
(`/audit-logs` und `/audit-logs/export`).
- **Getestet über HTTP gegen eine Wegwerf-DB**, inkl. echtem Gegenbuch-Lauf
mit SSH-signiertem lokalem Repo: Erstsiegeln mit `SEAL` → 200; zweites mit
`SEAL`**400**; mit `RESEAL` → 200; beide Alarmkanal-Ereignisse mit
korrekter Severity vorhanden. Wäsche (Zeile 7 gelöscht → RESEAL) → CRM
meldet `valid:true`, **Gegenbuch exit=2**. Bestätigung: falsche Wurzel →
weiter exit 2, zu kurzer Wert → weiter exit 2, richtige Wurzel → exit 0
und beglaubigt, Folgelauf ruhig.
- **Nachgeholt, was der Tester nicht herstellen konnte:** vollständig
unsigniertes Protokoll (Platzhalter-`AUDIT_HMAC_KEY`) → `kein_siegel`
statt der früheren falschen Entwarnung `nicht_noetig`, und
`seal-backlog` nennt den fehlenden Schlüssel als nächsten Schritt die
Warnung ist also auflösbar. Gegenrichtung (Log beginnt signiert) →
weiterhin `nicht_noetig`, R183-03 bleibt behoben.
- Dateien: `backend/src/controllers/auditLog.controller.ts`,
`backend/prisma/schema.prisma` + Migration, `tools/audit-notary/notary.mjs`,
`tools/audit-notary/{docker-compose.yml,.env.example,README.md}`,
`frontend/src/services/api.ts`, `frontend/src/pages/settings/Monitoring.tsx`
- [x] **👁️ Integritätsstatus in der Oberfläche (Einstellungen → Audit-Protokoll)** (2026-08-26)
- Bisher war der Zustand der Hash-Kette nur per `POST /api/audit-logs/verify`
einsehbar also praktisch nur für das Gegenbuch und für jemanden mit