OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

AuditChecksumPayloadTest.php

4 KB
<?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');
    });
};