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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user