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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user