rehash/cleanup: Vorzustand sichern + Bestaetigung verlangen (Pentest R170-01)

POST /audit-logs/rehash rechnet die Kette mit dem HMAC-Schluessel neu und macht
sie damit wieder stimmig - auch wenn sie vorher berechtigte Beanstandungen
enthielt. Der Anker schuetzt gegen einen DB-Schreiber ohne Schluessel, nicht
gegen einen Admin mit audit:admin. Der bisherige Marker hielt nur fest, DASS
rehasht wurde, nicht WAS dabei verschwand.

Fix 1: Vor dem Rehash wird verifyIntegrity() erhoben und samt Kettenkopf im
Marker gesichert - Anzahl geprueft, Listen der manipulierten Zeilen, der
Ketten-Luecken, der Luecken ohne dokumentierte Loeschung, der nicht pruefbaren.
Dazu ausloesender Benutzer und IP statt pauschal "system". Der Marker entsteht
nach dem Rehash, ist Teil der neuen Kette und signiert.

Fix 2: rehash verlangt {"confirm":"REHASH"}, cleanup verlangt
{"confirm":"CLEANUP"}. Beide wurden bei blinder Methoden-Erkundung per POST
unbeabsichtigt ausgeloest; ein tastender Aufruf laeuft jetzt in 400.

Verifiziert: blinder POST -> 400 ohne Wirkung; mit Bestaetigung laeuft der
Rehash und der Marker enthaelt Ausloeser, Vorbefund (1 manipuliert, 7 Luecken
mit exakten IDs) und Kettenkopf.

Hinweis: Der Test hat auf der DEV-Datenbank real rehasht, die dortigen
historischen Beanstandungen sind damit geglaettet. Staging/Prod unberuehrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 21:16:08 +02:00
co-authored by Claude Opus 5
parent 791711ca58
commit 32c9efacda
3 changed files with 102 additions and 5 deletions
+42 -4
View File
@@ -869,7 +869,27 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
/**
* Hash-Kette komplett neu berechnen (Reparatur)
*/
export async function rehashAll(): Promise<{ rehashedCount: number }> {
export async function rehashAll(
ausgeloestVon?: { userEmail?: string; ipAddress?: string },
): Promise<{ rehashedCount: number }> {
// ZUSTAND VOR DEM REHASH SICHERN (Pentest R170-01)
//
// Ein Rehash macht die Kette rechnerisch wieder stimmig auch dann, wenn sie
// vorher berechtigte Beanstandungen enthielt. Der bisherige Marker hielt nur
// fest, DASS rehasht wurde, nicht WAS dabei verschwand. Wer `audit:admin`
// besitzt, konnte damit Spuren glattziehen, ohne dass hinterher erkennbar
// war, welche.
//
// Deshalb wird der Befund samt Kettenkopf VOR dem Rehash erhoben und im
// Marker mitgeschrieben. Der Marker entsteht nach dem Rehash, ist selbst
// Teil der neuen Kette und signiert; entfernen liesse er sich nur unter
// Hinterlassung einer Luecke.
const vorher = await verifyIntegrity();
const kopf = await prisma.auditLog.findFirst({
orderBy: { id: 'desc' },
select: { id: true, hash: true, hashVersion: true, createdAt: true },
});
const logs = await prisma.auditLog.findMany({
orderBy: { id: 'asc' },
select: {
@@ -925,15 +945,33 @@ export async function rehashAll(): Promise<{ rehashedCount: number }> {
// haengt sich an die neu berechnete Kette; entfernen liesse er sich nur unter
// Hinterlassung einer Luecke.
await createAuditLog({
userEmail: 'system',
userEmail: ausgeloestVon?.userEmail || 'system',
userRole: 'System',
action: 'UPDATE',
resourceType: 'AuditLog',
resourceLabel: `Hash-Kette neu berechnet (${count} Einträge) Beweiskraft der Vergangenheit zurückgesetzt`,
resourceLabel:
`Hash-Kette neu berechnet (${count} Einträge) Beweiskraft der Vergangenheit zurückgesetzt` +
(vorher.valid
? ' Kette war vorher unbeanstandet'
: ` vorher beanstandet: ${vorher.tamperedEntries.length} manipuliert, ` +
`${vorher.chainGaps.length} Lücken`),
endpoint: '/api/audit-logs/rehash',
httpMethod: 'POST',
ipAddress: 'system',
ipAddress: ausgeloestVon?.ipAddress || 'system',
sensitivity: 'CRITICAL',
// Befund VOR dem Rehash ohne das waere nach dem Rehash nicht mehr
// nachvollziehbar, was uebertuencht wurde.
changesBefore: {
geprueft: vorher.checkedCount,
manipuliert: vorher.tamperedEntries,
ketten_luecken: vorher.chainGaps,
luecken_ohne_dokumentierte_loeschung: vorher.unexplainedGaps,
nicht_pruefbar: vorher.unverifiableEntries,
kettenkopf: kopf
? { id: kopf.id, hash: kopf.hash, hashVersion: kopf.hashVersion, createdAt: kopf.createdAt }
: null,
},
changesAfter: { neuBerechnet: count },
success: true,
});