OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

ExtensionInstallGuardTest.php

4 KB
<?php

declare(strict_types=1);

use OpenNIT\Services\DrawioManager;
use OpenNIT\Services\TinyMceManager;

/**
 * Deckt die Schutzmaßnahmen der Erweiterungs-Installer ab.
 *
 * draw.io und TinyMCE werden in den Webroot entpackt. Damit dort nichts landet,
 * was der Server ausführen könnte, greifen drei voneinander unabhängige Stufen:
 * feste Bezugsquelle, Ausschluss ausführbarer Endungen beim Übernehmen und ein
 * Ausführungsschutz im Webroot.
 */
return static function (): void {

    $wurzel = dirname(__DIR__);

    /** Ruft die Ausschlussliste der Endungen einer Klasse ab. */
    $denylist = static function (string $klasse): array {
        $r = new ReflectionClass($klasse);
        return (array)$r->getConstant('DENIED_EXTENSIONS');
    };

    // ----------------------------------------------------------------
    // Bezugsquelle
    // ----------------------------------------------------------------

    test('Die Bezugsquellen sind auf das jeweilige Projekt festgelegt', function () {
        assertSame(
            'https://github.com/jgraph/drawio/releases/download/',
            DrawioManager::RELEASE_URL_PREFIX
        );
        assertSame(
            'https://registry.npmjs.org/tinymce/-/',
            TinyMceManager::TARBALL_URL_PREFIX
        );
    });

    test('Ein fremdes Repository erfüllt die Bezugsquelle nicht', function () {
        // Genau der Fall, den eine reine „beginnt mit github.com"-Prüfung zuließe.
        $fremd = 'https://github.com/angreifer/paket/releases/download/v1/schaden.war';
        assertFalse(str_starts_with($fremd, DrawioManager::RELEASE_URL_PREFIX));

        $fremdesPaket = 'https://registry.npmjs.org/boesepaket/-/boesepaket-1.0.0.tgz';
        assertFalse(str_starts_with($fremdesPaket, TinyMceManager::TARBALL_URL_PREFIX));
    });

    // ----------------------------------------------------------------
    // Endungs-Ausschluss
    // ----------------------------------------------------------------

    test('Serverseitig ausführbare Endungen sind ausgeschlossen', function () use ($denylist) {
        $verboten = ['php', 'phtml', 'phar', 'php5', 'php8', 'cgi', 'pl', 'py', 'sh', 'jsp', 'asp', 'htaccess'];
        foreach ([DrawioManager::class, TinyMceManager::class] as $klasse) {
            foreach ($verboten as $ext) {
                assertTrue(
                    in_array($ext, $denylist($klasse), true),
                    $klasse . ' übernimmt .' . $ext
                );
            }
        }
    });

    test('Ressourcen der Pakete bleiben erhalten', function () use ($denylist) {
        // Eine Ausschlussliste statt einer Auswahlliste: draw.io und TinyMCE
        // bringen Sprachdateien, Formenbibliotheken und Skins mit, die für die
        // Funktion nötig sind und nicht stillschweigend wegfallen dürfen.
        foreach (['js', 'css', 'html', 'png', 'svg', 'woff2', 'properties', 'xml', 'json', 'md'] as $ext) {
            assertFalse(in_array($ext, $denylist(DrawioManager::class), true), 'drawio verwirft .' . $ext);
            assertFalse(in_array($ext, $denylist(TinyMceManager::class), true), 'tinymce verwirft .' . $ext);
        }
    });

    // ----------------------------------------------------------------
    // Ausführungsschutz im Webroot
    // ----------------------------------------------------------------

    test('Der Webroot lässt nur die Einstiegsskripte ausführen', function () use ($wurzel) {
        $htaccess = (string)file_get_contents($wurzel . '/public/.htaccess');

        assertTrue(
            str_contains($htaccess, 'php|phtml'),
            'Sperre für Skript-Endungen fehlt'
        );
        assertTrue(
            str_contains($htaccess, '^(index|install)\.php$'),
            'Ausnahme für die Einstiegsskripte fehlt'
        );
    });

    test('Das Projektverzeichnis schützt sich gegen eine falsch gesetzte DocumentRoot', function () use ($wurzel) {
        $htaccess = $wurzel . '/.htaccess';
        assertTrue(is_file($htaccess), 'Wurzel-.htaccess fehlt');

        $inhalt = (string)file_get_contents($htaccess);
        foreach (['src', 'config', 'storage'] as $verzeichnis) {
            assertTrue(str_contains($inhalt, $verzeichnis), 'Kein Schutz für ' . $verzeichnis);
        }
        assertTrue(str_contains($inhalt, '^\\.'), 'Kein Schutz für versteckte Dateien');
    });
};