OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

TemplateEventAttributeTest.php

3 KB
<?php

declare(strict_types=1);

/**
 * Wacht über serverseitige Werte in HTML-Ereignisattributen.
 *
 * `json_encode()` allein macht einen Wert nicht attributsicher: Apostroph und
 * Anführungszeichen bleiben unverändert und beenden das Attribut, wodurch ein
 * eigenes Ereignisattribut eingeschleust werden kann. Sicher ist der Wert erst
 * mit HTML-Maskierung (`$this->e(...)` / `htmlspecialchars`) oder mit den
 * JSON-Flags, die beide Zeichenarten hexadezimal schreiben.
 */
return static function (): void {

    $templateVerzeichnis = dirname(__DIR__) . '/templates';

    /** Alle Template-Dateien. */
    $dateien = static function (string $verzeichnis): array {
        $gefunden = [];
        $lauf = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($verzeichnis));
        foreach ($lauf as $eintrag) {
            if ($eintrag->isFile() && $eintrag->getExtension() === 'php') {
                $gefunden[] = $eintrag->getPathname();
            }
        }
        sort($gefunden);
        return $gefunden;
    };

    test('Es werden überhaupt Templates geprüft', function () use ($dateien, $templateVerzeichnis) {
        assertTrue(count($dateien($templateVerzeichnis)) > 100, 'Template-Verzeichnis nicht gefunden');
    });

    test('json_encode in Ereignisattributen ist attributsicher maskiert', function () use ($dateien, $templateVerzeichnis) {
        $verstoesse = [];

        foreach ($dateien($templateVerzeichnis) as $datei) {
            $inhalt = (string)file_get_contents($datei);

            // Ereignisattribut (onclick, onsubmit, …) bis zum schließenden Quote,
            // sofern darin ein json_encode steht.
            if (!preg_match_all(
                '/\bon[a-z]+\s*=\s*(["\'])(?:(?!\1).){0,600}?json_encode\s*\((?:(?!\1).){0,600}?\1/is',
                $inhalt,
                $treffer,
                PREG_OFFSET_CAPTURE
            )) {
                continue;
            }

            foreach ($treffer[0] as $fund) {
                $attribut = $fund[0];
                $zeile    = substr_count(substr($inhalt, 0, $fund[1]), "\n") + 1;

                $maskiert = preg_match('/(htmlspecialchars|->e)\s*\(\s*json_encode/i', $attribut) === 1;
                $hexFlags = preg_match('/JSON_HEX_APOS/i', $attribut) === 1
                    && preg_match('/JSON_HEX_QUOT/i', $attribut) === 1;

                if (!$maskiert && !$hexFlags) {
                    $verstoesse[] = str_replace($templateVerzeichnis . '/', '', $datei) . ':' . $zeile;
                }
            }
        }

        assertSame(
            [],
            $verstoesse,
            "Ungeschütztes json_encode in Ereignisattributen:\n" . implode("\n", $verstoesse)
        );
    });
};