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:
@@ -135,6 +135,24 @@ if (!SIGN && process.env.NOTARY_INSECURE_ACK !== 'mir-ist-klar-dass-das-ungeschu
|
||||
const CRM_EMAIL = process.env.CRM_EMAIL;
|
||||
const CRM_PASSWORD = process.env.CRM_PASSWORD;
|
||||
|
||||
// Bestaetigung fuer einen Wechsel der Siegelwurzel (Pentest R185-01).
|
||||
//
|
||||
// Ein Wechsel loest Alarm aus – und der Alarm bricht ab, BEVOR der neue Stand
|
||||
// ins Gegenbuch kommt. Ohne Bestaetigungsweg wuerde deshalb auch ein voellig
|
||||
// legitimes Siegeln von da an bei jedem Lauf erneut alarmieren, ohne dass der
|
||||
// Betreiber es je aufloesen koennte. Genau daran stirbt eine Warnung
|
||||
// (dieselbe Lehre wie R183-03).
|
||||
//
|
||||
// Bestaetigt wird deshalb nicht pauschal mit „true“, sondern mit der WURZEL,
|
||||
// die man akzeptiert – mindestens 16 Zeichen. Ein versehentlich stehen
|
||||
// gelassener Wert passt beim naechsten Wechsel nicht mehr und kann darum
|
||||
// keinen weiteren Austausch stillschweigend durchwinken. Das ist der
|
||||
// Unterschied zu NOTARY_GENESIS_ACK, wo genau diese Falle dokumentiert
|
||||
// werden musste.
|
||||
const SEAL_ACK = (process.env.NOTARY_SEAL_ACK || '').trim();
|
||||
const siegelBestaetigt = (wurzel) =>
|
||||
SEAL_ACK.length >= 16 && !!wurzel && wurzel.startsWith(SEAL_ACK);
|
||||
|
||||
if (!CRM_URL) {
|
||||
console.error('CRM_URL muss gesetzt sein.');
|
||||
process.exit(1);
|
||||
@@ -774,10 +792,59 @@ if (letzter) {
|
||||
if (letzter.sealLeafCount > 0 && aktuell.sealLeafCount === 0) {
|
||||
alarm('Die Blattwerte des Bestandssiegels wurden entfernt.');
|
||||
}
|
||||
// Wechsel der Siegelwurzel ist ein ALARM, kein Hinweis (Pentest R185-01).
|
||||
//
|
||||
// Vorher stand hier ein console.warn und der Rueckgabecode blieb 0 – also
|
||||
// genau das Muster, das wir dem CRM zweimal angekreidet haben: die Warnung
|
||||
// steht in der Prosa, die Maschine meldet „in Ordnung“. Das ist hier
|
||||
// besonders teuer, weil `valid` ein ERSETZENDES Siegel per Konstruktion
|
||||
// ueberlebt: Wer eine Altzeile per Datenbankzugriff entfernt und danach neu
|
||||
// siegelt, bekommt eine passende Wurzel und eine beglaubigte Luecke. Die
|
||||
// Vollpruefung sagt dann `true`. Der Wurzelwechsel ist der EINZIGE
|
||||
// maschinell erkennbare Anker dagegen – und der gehoert in den Alarm.
|
||||
//
|
||||
// Ein legitimes Neu-Siegeln loest hier einmal aus. Das ist gewollt: ein
|
||||
// Austausch der Beweisgrundlage soll einmal wehtun und bestaetigt werden,
|
||||
// statt lautlos durchzulaufen.
|
||||
if (letzter.sealRoot && aktuell.sealRoot && letzter.sealRoot !== aktuell.sealRoot) {
|
||||
console.warn(
|
||||
`HINWEIS: Das Bestandssiegel wurde erneuert (${letzter.sealRoot.slice(0, 12)}… → ` +
|
||||
`${aktuell.sealRoot.slice(0, 12)}…). Legitim nach einem Retention-Lauf – sonst prüfen.`,
|
||||
if (siegelBestaetigt(aktuell.sealRoot)) {
|
||||
console.log(
|
||||
`Siegelwechsel bestätigt (NOTARY_SEAL_ACK): ${letzter.sealRoot.slice(0, 16)}… → ` +
|
||||
`${aktuell.sealRoot.slice(0, 16)}…. Die neue Wurzel wird beglaubigt.\n` +
|
||||
' NOTARY_SEAL_ACK danach wieder leeren.',
|
||||
);
|
||||
} else alarm(
|
||||
'Das Bestandssiegel wurde ERSETZT – die beglaubigte Grundlage ist eine andere.\n' +
|
||||
` beglaubigt: ${letzter.sealRoot.slice(0, 16)}… (${letzter.sealLeafCount} Blätter)\n` +
|
||||
` jetzt : ${aktuell.sealRoot.slice(0, 16)}… (${aktuell.sealLeafCount} Blätter)\n` +
|
||||
'Ein erneutes Siegeln schreibt den AKTUELLEN Stand des Altbestands fest. Wurden\n' +
|
||||
'vorher Einträge entfernt, sind deren Lücken danach beglaubigt und die Prüfung im\n' +
|
||||
'CRM meldet wieder valid:true – dieser Wurzelwechsel ist die einzige Spur davon.\n' +
|
||||
'Wenn das keine geplante Maßnahme war: Im CRM das Ereignis AUDIT_SEAL_CHANGED und\n' +
|
||||
'die CRITICAL-Zeile zu /api/audit-logs/seal-backlog ansehen (wer, wann, Vorbefund).\n' +
|
||||
'War es geplant, den Wechsel bestätigen und danach wieder leeren:\n' +
|
||||
` NOTARY_SEAL_ACK=${aktuell.sealRoot.slice(0, 32)}`,
|
||||
);
|
||||
}
|
||||
// Erstsiegelung: vorher nichts beglaubigt, jetzt eine Wurzel. Das ist der
|
||||
// eine legitime Einrichtungsschritt – aber auch das Fenster, in dem ein
|
||||
// beschnittener Altbestand einmalig festgeschrieben werden koennte. Deshalb
|
||||
// ebenfalls melden, mit eigenem Text statt stillschweigend zu uebernehmen.
|
||||
if (!letzter.sealRoot && aktuell.sealRoot) {
|
||||
if (siegelBestaetigt(aktuell.sealRoot)) {
|
||||
console.log(
|
||||
`Erstsiegelung bestätigt (NOTARY_SEAL_ACK): ${aktuell.sealRoot.slice(0, 16)}…. ` +
|
||||
'Die Wurzel wird beglaubigt.\n NOTARY_SEAL_ACK danach wieder leeren.',
|
||||
);
|
||||
} else alarm(
|
||||
'Erstmals ein Bestandssiegel gesetzt – bisher war keines beglaubigt.\n' +
|
||||
` Wurzel: ${aktuell.sealRoot.slice(0, 16)}… (${aktuell.sealLeafCount} Blätter)\n` +
|
||||
'War das dein Einrichtungsschritt, ist alles in Ordnung: Der nächste Lauf läuft\n' +
|
||||
'wieder auf 0, sobald diese Wurzel im Gegenbuch steht.\n' +
|
||||
'War es das NICHT, dann hat jemand den Stand des Altbestands festgeschrieben –\n' +
|
||||
'samt aller Lücken, die zu diesem Zeitpunkt bestanden.\n' +
|
||||
'Zum Bestätigen setzen und danach wieder leeren:\n' +
|
||||
` NOTARY_SEAL_ACK=${aktuell.sealRoot.slice(0, 32)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user