Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
use OpenNIT\Audit\ChecksumPayload;
/**
* Deckt die Eindeutigkeit der Feldgrenzen in der Audit-Prüfsumme ab.
*
* Die Prüfsumme belegt den Inhalt eines Eintrags nur dann, wenn sich aus der
* signierten Zeichenkette genau eine Aufteilung in Felder ergibt. Bei der
* früheren Verkettung mit `|` war das nicht der Fall: Mehrere Felder tragen
* frei wählbaren Text, sodass sich dieselben Bytes anders auf die Felder
* verteilen ließen – für einen Angreifer mit Schreibzugriff auf die Datenbank
* genau die Lücke, gegen die die Kette schützen soll.
*/
return static function (): void {
// ----------------------------------------------------------------
// Die Lücke im früheren Verfahren
// ----------------------------------------------------------------
test('Die frühere Verkettung lässt zwei Aufteilungen derselben Bytes zu', function () {
// Ein Objektname, der den Trenner enthält, verschiebt die Grenze zum
// Folgefeld – beide Belegungen ergeben dieselbe signierte Zeichenkette.
$a = ['Anna', 'Meier|admin'];
$b = ['Anna|Meier', 'admin'];
assertSame(ChecksumPayload::pipeJoined($a), ChecksumPayload::pipeJoined($b));
});
test('Längenpräfixe trennen die beiden Aufteilungen', function () {
$a = ['Anna', 'Meier|admin'];
$b = ['Anna|Meier', 'admin'];
assertTrue(
ChecksumPayload::lengthPrefixed($a) !== ChecksumPayload::lengthPrefixed($b),
'Verschobene Feldgrenze ergibt dieselbe Serialisierung'
);
});
test('Auch ein leeres Nachbarfeld verschiebt die Grenze nicht mehr', function () {
// Leere Felder sind der Regelfall (object_type, object_id, JSON-Spalten)
// und waren in der Verkettung von ihrem Nachbarn nicht zu unterscheiden.
assertSame(ChecksumPayload::pipeJoined(['', 'x']), ChecksumPayload::pipeJoined(['', 'x']));
assertTrue(
ChecksumPayload::lengthPrefixed(['', 'x']) !== ChecksumPayload::lengthPrefixed(['x', '']),
'Leeres Feld bleibt von seinem Nachbarn ununterscheidbar'
);
});
// ----------------------------------------------------------------
// Eigenschaften der neuen Serialisierung
// ----------------------------------------------------------------
test('Gleiche Felder ergeben dieselbe Serialisierung', function () {
$felder = ['a', 'b', 'c'];
assertSame(
ChecksumPayload::lengthPrefixed($felder),
ChecksumPayload::lengthPrefixed(['a', 'b', 'c'])
);
});
test('Die Länge zählt Bytes, nicht Zeichen', function () {
// Ein Umlaut belegt in UTF-8 zwei Bytes; nur die Byte-Länge macht die
// Zerlegung unabhängig von der Zeichenkodierung eindeutig.
assertSame('8:audit-v22:ä', ChecksumPayload::lengthPrefixed(['ä']));
});
test('Die Serialisierungen können einander nie gleichen', function () {
// Die Kennzeichnung geht als erstes Feld mit ein, damit eine
// längenpräfixierte Zeichenkette nie einer verketteten entspricht.
$felder = ['x', 'y'];
assertTrue(
ChecksumPayload::lengthPrefixed($felder) !== ChecksumPayload::pipeJoined($felder),
'Die beiden Verfahren sind nicht unterscheidbar'
);
assertTrue(
str_starts_with(ChecksumPayload::lengthPrefixed($felder), '8:audit-v2'),
'Kennzeichnung des Verfahrens fehlt'
);
});
test('Der Trenner im Inhalt bleibt Inhalt', function () {
// Was früher die Feldgrenze war, ist jetzt ein Zeichen wie jedes andere.
$mitTrenner = ChecksumPayload::lengthPrefixed(['a|b']);
assertTrue(str_contains($mitTrenner, '3:a|b'), 'Der Wert wird nicht unverändert übernommen');
});
};