Manifest-Kanal abgesichert (Pentest R171-01 HIGH, R171-03 LOW)

verifyIntegrity vertraute Loeschungs-Manifesten bedingungslos, ohne zu pruefen,
ob die Traegerzeile signiert und gueltig ist. Das Manifest steht in
changesAfter, und dieses Feld ist erst ab Hash-Version 2 mitgehasht - auf
V1-Altzeilen also voellig unauthentifiziert. Ein Angreifer konnte in eine
beliebige V1-Zeile ein erfundenes Manifest schreiben, ohne deren Hash zu
aendern, und damit eigene Loeschungen als "erklaert" ausweisen. Damit fiel
zugleich die Eskalation an signierten Zeilen aus - Anker UND Versionsgrenze
umgangen.

Fix: Ein Manifest zaehlt nur, wenn die Traegerzeile laut Versionsgrenze Stufe 3
sein muss, dies auch deklariert, und ihre HMAC-Signatur mit einem
konfigurierten Schluessel aufgeht. Ohne Schluessel gibt es keine gueltigen
Traeger - Luecken bleiben dann unerklaert, die sichere Richtung.

R171-03: Eskalierte Luecken standen in tamperedEntries und chainGaps,
invalidEntries zaehlte sie doppelt. Jetzt entdoppelt.

Verifiziert: boeswillige Loeschung -> Befund; erfundenes Manifest in
herabgestufter Traegerzeile -> ignoriert, Luecke bleibt Befund, Traegerzeile
selbst beanstandet; legitimes signiertes Manifest erklaert die Luecke
weiterhin. tsc gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 21:42:48 +02:00
co-authored by Claude Opus 5
parent 32c9efacda
commit 2b0af772a2
2 changed files with 57 additions and 8 deletions
+30 -8
View File
@@ -720,16 +720,36 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
return 1;
};
// Dokumentierte Loeschungen einlesen: Luecken, die auf einen protokollierten
// Retention-Cleanup zurueckgehen, sind erklaert alle anderen nicht. Vorher
// war das Manifest rein informativ und wurde von der Pruefung ignoriert,
// wodurch sich eine boeswillige Loeschung als "harmloser Gap" tarnen konnte.
// Dokumentierte Loeschungen einlesen NUR aus signierten, selbst gueltigen
// Traegerzeilen (Pentest R171-01).
//
// Vorher wurde jedem Manifest bedingungslos geglaubt. Das Manifest steht in
// `changesAfter`, und dieses Feld ist erst ab Hash-Version 2 mitgehasht auf
// V1-Altzeilen also voellig unauthentifiziert. Ein Angreifer konnte daher in
// eine BELIEBIGE V1-Zeile ein erfundenes Manifest schreiben, ohne deren Hash
// zu veraendern, und damit eigene Loeschungen und Luecken als "erklaert"
// ausweisen. Das hebelte zugleich die Eskalation an signierten Zeilen aus
// also Anker UND Versionsgrenze.
//
// Deshalb zaehlt ein Manifest nur, wenn seine Traegerzeile
// 1. laut Versionsgrenze Stufe 3 sein MUSS und dies auch deklariert, und
// 2. ihre HMAC-Signatur mit einem konfigurierten Schluessel aufgeht.
// Ohne Schluessel gibt es keine gueltigen Traeger dann bleiben Luecken
// unerklaert, was die sichere Richtung ist.
const deletionRanges: Array<{ from: number; to: number }> = [];
const manifestRows = await prisma.auditLog.findMany({
const manifestKandidaten = await prisma.auditLog.findMany({
where: { resourceType: 'AuditLog', action: 'DELETE', endpoint: '/api/audit-logs/cleanup' },
select: { changesAfter: true },
});
for (const row of manifestRows) {
const manifestSchluessel = [auditHmacKey(), ...auditHmacKeysOld()].filter(
(k): k is string => !!k,
);
for (const row of manifestKandidaten) {
if (erwarteteVersion(row.id) !== 3 || row.hashVersion < 3) continue;
if (manifestSchluessel.length === 0) continue;
const signaturOk = manifestSchluessel.some(
(k) => row.hash === generateHashV3(row as unknown as AuditHashV2Input, k),
);
if (!signaturOk) continue;
try {
const parsed = JSON.parse(row.changesAfter || '{}');
for (const m of parsed.manifest || []) {
@@ -853,7 +873,9 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
}
}
const invalidEntries = [...tamperedEntries, ...chainGaps].sort((a, b) => a - b);
// Eskalierte Luecken stehen sowohl in tamperedEntries als auch in chainGaps;
// ohne Entdopplung zaehlte dieselbe Zeile zweimal (Pentest R171-03).
const invalidEntries = [...new Set([...tamperedEntries, ...chainGaps])].sort((a, b) => a - b);
return {
valid: invalidEntries.length === 0,
+27
View File
@@ -97,6 +97,33 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt
- [x] **🔒 Manifest-Kanal abgesichert (Pentest R171-01, HIGH; R171-03, LOW)** (2026-08-18)
- **R171-01:** `verifyIntegrity` vertraute Loeschungs-Manifesten
bedingungslos ohne zu pruefen, ob die Traegerzeile ueberhaupt signiert
und gueltig ist. Das Manifest steht in `changesAfter`, und dieses Feld ist
erst ab Hash-Version 2 mitgehasht, auf **V1-Altzeilen also voellig
unauthentifiziert**. Ein Angreifer konnte in eine BELIEBIGE V1-Zeile ein
erfundenes Manifest schreiben, ohne deren Hash zu veraendern, und damit
eigene Loeschungen als „erklaert“ ausweisen womit zugleich die
Eskalation an signierten Zeilen ausfiel. Damit waren **Anker UND
Versionsgrenze** umgangen. Der Pentester hat es live gezeigt; es war genau
die Stelle, die ich beim Uebergeben als meine schwaechste benannt hatte.
- Fix: Ein Manifest zaehlt nur noch, wenn seine Traegerzeile (1) laut
Versionsgrenze Stufe 3 sein MUSS und dies auch deklariert und (2) ihre
HMAC-Signatur mit einem konfigurierten Schluessel aufgeht. Ohne Schluessel
gibt es keine gueltigen Traeger dann bleiben Luecken unerklaert, was die
sichere Richtung ist.
- **R171-03:** Eskalierte Luecken standen in `tamperedEntries` UND
`chainGaps`; `invalidEntries` zaehlte sie doppelt. Jetzt entdoppelt.
- Verifiziert: boeswillige Loeschung → Befund; erfundenes Manifest in
herabgestufter Traegerzeile → **ignoriert**, Luecke bleibt Befund und die
Traegerzeile wird selbst beanstandet; **legitimes signiertes Manifest
erklaert die Luecke weiterhin** (Funktion nicht kaputt). `tsc` gruen.
- Hinweis: Auf der Dev-DB existiert kein V1-Bestand mehr (der Rehash-Test aus
R170-01 hat alles auf V3 gehoben), der Angriff wurde daher mit einer gezielt
herabgestuften Traegerzeile nachgestellt. Auf Staging/Prod ist der
V1-Altbestand real vorhanden.
- [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