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
+32 -1
View File
@@ -198,7 +198,26 @@ export async function verifyIntegrity(req: AuthRequest, res: Response) {
*/ */
export async function rehashAll(req: AuthRequest, res: Response) { export async function rehashAll(req: AuthRequest, res: Response) {
try { try {
const result = await auditService.rehashAll(); // Ausdrueckliche Bestaetigung verlangen (Pentest R170-01).
//
// Ein Rehash macht die Kette rechnerisch stimmig und setzt damit die
// Beweiskraft der Vergangenheit zurueck - das darf kein Nebeneffekt eines
// versehentlichen oder tastenden POST sein. Genau so wurde der Endpunkt
// bei einer Methoden-Erkundung unbeabsichtigt ausgeloest.
if (req.body?.confirm !== 'REHASH') {
res.status(400).json({
success: false,
error:
'Rehash setzt die Beweiskraft der bestehenden Einträge zurück und ist nicht ' +
'umkehrbar. Zum Bestätigen {"confirm":"REHASH"} mitsenden.',
});
return;
}
const result = await auditService.rehashAll({
userEmail: req.user?.email,
ipAddress: req.ip || (req.socket as any)?.remoteAddress,
});
res.json({ res.json({
success: true, success: true,
data: result, data: result,
@@ -259,6 +278,18 @@ export async function updateRetentionPolicy(req: AuthRequest, res: Response) {
*/ */
export async function runRetentionCleanup(req: AuthRequest, res: Response) { export async function runRetentionCleanup(req: AuthRequest, res: Response) {
try { try {
// Auch hier ausdrueckliche Bestaetigung: Cleanup loescht Audit-Zeilen
// endgueltig und reisst dabei die Kette auf (Pentest R170-01).
if (req.body?.confirm !== 'CLEANUP') {
res.status(400).json({
success: false,
error:
'Cleanup löscht Audit-Einträge endgültig. Zum Bestätigen ' +
'{"confirm":"CLEANUP"} mitsenden.',
});
return;
}
const result = await auditService.runRetentionCleanup(); const result = await auditService.runRetentionCleanup();
res.json({ res.json({
+42 -4
View File
@@ -869,7 +869,27 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
/** /**
* Hash-Kette komplett neu berechnen (Reparatur) * 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({ const logs = await prisma.auditLog.findMany({
orderBy: { id: 'asc' }, orderBy: { id: 'asc' },
select: { 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 // haengt sich an die neu berechnete Kette; entfernen liesse er sich nur unter
// Hinterlassung einer Luecke. // Hinterlassung einer Luecke.
await createAuditLog({ await createAuditLog({
userEmail: 'system', userEmail: ausgeloestVon?.userEmail || 'system',
userRole: 'System', userRole: 'System',
action: 'UPDATE', action: 'UPDATE',
resourceType: 'AuditLog', 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', endpoint: '/api/audit-logs/rehash',
httpMethod: 'POST', httpMethod: 'POST',
ipAddress: 'system', ipAddress: ausgeloestVon?.ipAddress || 'system',
sensitivity: 'CRITICAL', 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, success: true,
}); });
+28
View File
@@ -97,6 +97,34 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt ## ✅ Erledigt
- [x] **🧾 rehash/cleanup: Vorzustand sichern + Bestaetigung verlangen (Pentest R170-01)** (2026-08-18)
- Fund: `POST /audit-logs/rehash` (audit:admin) rechnet die Kette MIT dem
HMAC-Schluessel neu und macht sie damit wieder stimmig auch wenn sie
vorher berechtigte Beanstandungen enthielt. Der Anker schuetzt also 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 **Vorzustand im Marker**: Vor dem Rehash wird `verifyIntegrity()`
erhoben und samt Kettenkopf (id, hash, hashVersion) im Marker gesichert:
Anzahl geprueft, Listen der manipulierten Zeilen, der Ketten-Luecken, der
Luecken ohne dokumentierte Loeschung und der nicht pruefbaren. Dazu
ausloesender Benutzer und IP (bisher stand dort pauschal „system“).
Der Marker entsteht nach dem Rehash, ist Teil der neuen Kette und
signiert entfernen ginge nur unter Hinterlassung einer Luecke.
- Fix 2 **Bestaetigung verlangen**: `rehash` erfordert
`{"confirm":"REHASH"}`, `cleanup` erfordert `{"confirm":"CLEANUP"}`.
Der Pentester hatte beide bei blinder Methoden-Erkundung per POST
unbeabsichtigt ausgeloest; ein tastender Aufruf laeuft jetzt in 400.
- Verifiziert: blinder POST auf beide Endpunkte → 400 ohne Wirkung; mit
Bestaetigung → Rehash laeuft, Marker enthaelt Ausloeser, Vorbefund
(1 manipuliert, 7 Luecken mit exakten IDs) und Kettenkopf.
- **Offengelegt:** Dieser Test hat auf der DEV-Datenbank real rehasht die
dortigen historischen Beanstandungen sind rechnerisch geglaettet
(`valid = true`). Genau der beschriebene Effekt; der Vorzustand steht
jetzt aber im Marker. Staging/Prod unberuehrt.
- Offen (Betreiber-Entscheidung): Off-Site-Notarisierung des Kettenkopfes.
Erst sie deckt den Fall „Admin mit audit:admin“ vollstaendig ab.
- [x] **🔒 Refresh-Kulanz idempotent: stiller Session-Fork geschlossen (Pentest R168-01, HIGH)** (2026-08-18) - [x] **🔒 Refresh-Kulanz idempotent: stiller Session-Fork geschlossen (Pentest R168-01, HIGH)** (2026-08-18)
- Der Pentester hat genau die Frage beantwortet, die ich beim Uebergeben - Der Pentester hat genau die Frage beantwortet, die ich beim Uebergeben
gestellt hatte („laesst sich das Kulanzfenster ausnutzen?“) und zwar gestellt hatte („laesst sich das Kulanzfenster ausnutzen?“) und zwar