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:
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user