OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

SecurityAuditFixesTest.php

9 KB
<?php

declare(strict_types=1);

/**
 * Hält die vier Lücken fest, die das Sicherheits-Audit gefunden hat.
 *
 * Jeder Test prüft die Eigenschaft selbst, nicht ihre Schreibweise – ein
 * Umbau darf den Test brechen, wenn er die Eigenschaft aufgibt.
 */

use OpenNIT\Assets\RemoteAccess;
use OpenNIT\Backup\S3Client;
use OpenNIT\Backup\SelfBackupManager;

return function (): void {

    $wurzel = dirname(__DIR__);

    // ================================================================
    // Fernspeicher: nur Web-Protokolle
    // ================================================================

    test('Der Endpunkt des Fernspeichers muss über das Web gehen', function () {
        assertTrue(SelfBackupManager::endpunktTauglich('https://s3.example.org'), 'https wird abgewiesen');
        assertTrue(SelfBackupManager::endpunktTauglich('http://minio.intern:9000'), 'http wird abgewiesen');
        assertTrue(SelfBackupManager::endpunktTauglich('HTTPS://S3.EXAMPLE.ORG'), 'Großschreibung wird abgewiesen');

        // Diese Schemata würden die Übertragung auf das Dateisystem richten.
        assertFalse(SelfBackupManager::endpunktTauglich('file:///etc'), 'file:// gilt als tauglich');
        assertFalse(SelfBackupManager::endpunktTauglich('ftp://ablage.example.org'), 'ftp gilt als tauglich');
        assertFalse(SelfBackupManager::endpunktTauglich('/var/www'), 'ein blosser Pfad gilt als tauglich');
        assertFalse(SelfBackupManager::endpunktTauglich('https://'), 'Endpunkt ohne Rechnernamen gilt als tauglich');
        assertFalse(SelfBackupManager::endpunktTauglich(''), 'leerer Endpunkt gilt als tauglich');
    });

    test('Ein Dateipfad als Endpunkt liest und schreibt nichts', function () {
        $basis = sys_get_temp_dir() . '/onit-s3-test-' . bin2hex(random_bytes(4));
        @mkdir($basis . '/eimer', 0700, true);

        $opfer = $basis . '/eimer/unberuehrt.txt';
        file_put_contents($opfer, 'URSPRUNG');

        $nutzlast = $basis . '/nutzlast.bin';
        file_put_contents($nutzlast, 'FREMDER-INHALT');

        $client = new S3Client('file://' . $basis, 'us-east-1', 'eimer', 'k', 's', true);

        // Lesen: der Inhalt darf nicht als Antwortrumpf zurückkommen.
        $lesen = $client->listObjects('');
        assertFalse($lesen['ok'], 'Ein Dateipfad gilt als erreichbarer Speicher');
        assertFalse(str_contains($lesen['message'], 'URSPRUNG'), 'Der Dateiinhalt steht in der Meldung');

        // Schreiben: die vorhandene Datei bleibt, wie sie war.
        $client->putFile('unberuehrt.txt', $nutzlast);
        assertSame('URSPRUNG', (string)file_get_contents($opfer), 'Die Datei wurde überschrieben');

        @unlink($opfer);
        @unlink($nutzlast);
        @rmdir($basis . '/eimer');
        @rmdir($basis);
    });

    test('Eine abgewiesene Adresse übernimmt auch sonst nichts', function () use ($wurzel) {
        // Sonst stünde die Hälfte der Angaben schon in der Datenbank, während
        // das Formular einen Fehlschlag meldet.
        $quelle = (string)file_get_contents($wurzel . '/src/Controllers/Admin/SelfBackupAdminController.php');
        $beginn = strpos($quelle, 'public function saveSettings(');
        assertTrue($beginn !== false, 'saveSettings() fehlt');

        $pruefung = strpos($quelle, 'endpunktTauglich($endpunkt)', $beginn);
        $schreibt = strpos($quelle, 'speichereEinstellung(', $beginn);
        assertTrue($pruefung !== false, 'Der Endpunkt wird beim Speichern nicht geprüft');
        assertTrue($pruefung < $schreibt, 'Es wird geschrieben, bevor die Adresse geprüft ist');
    });

    test('curl bekommt die Protokollgrenze als zweite Schranke', function () use ($wurzel) {
        $quelle = (string)file_get_contents($wurzel . '/src/Backup/S3Client.php');
        assertTrue(str_contains($quelle, 'CURLOPT_PROTOCOLS'), 'Die Protokollgrenze fehlt');
        assertTrue(str_contains($quelle, 'CURLOPT_REDIR_PROTOCOLS'), 'Weiterleitungen sind unbegrenzt');
    });

    // ================================================================
    // Zurücksetzen: Zusage muss beim Server ankommen
    // ================================================================

    test('Das Zurücksetzen verlangt eine Zusage, die abgeschickt wird', function () use ($wurzel) {
        $seite = (string)file_get_contents($wurzel . '/templates/assets/show.php');

        // Ohne `name` erreicht das Kästchen den Server nie.
        assertTrue(
            (bool)preg_match('/id="inv-delete-wipe-ack"[^>]*name="intune_wipe_ack"/', $seite)
            || (bool)preg_match('/name="intune_wipe_ack"[^>]*id="inv-delete-wipe-ack"/', $seite),
            'Das Bestätigungskästchen wird nicht mitgeschickt'
        );

        $steuerung = (string)file_get_contents($wurzel . '/src/Controllers/AssetController.php');
        assertTrue(str_contains($steuerung, "intune_wipe_ack"), 'Der Server prüft die Zusage nicht');
        assertTrue(
            (bool)preg_match("/'wipe'.*intune_wipe_ack/s", $steuerung),
            'Die Prüfung hängt nicht am Zurücksetzen'
        );
    });

    // ================================================================
    // Verknüpfungen: Standortgrenze der Geräte
    // ================================================================

    test('Die Verknüpfungssuche trägt die Standortgrenze mit', function () use ($wurzel) {
        $manager = (string)file_get_contents($wurzel . '/src/Content/PageLinkManager.php');

        // Alle drei Wege an ein Gerät nehmen die Grenze entgegen.
        foreach (['getLinkedObjects', 'searchObjects', 'findObject'] as $weg) {
            assertTrue(
                (bool)preg_match('/function ' . $weg . '\((?:[^)]*)\$scopes/s', $manager),
                'Der Weg ' . $weg . ' kennt keine Grenze'
            );
        }
        assertTrue(str_contains($manager, 'scopeParts'), 'Die Grenze wird nicht eingesetzt');

        $steuerung = (string)file_get_contents($wurzel . '/src/Controllers/PageController.php');
        assertTrue(
            str_contains($steuerung, "locationScopeSql('view_assets', 'o.location_id')"),
            'Der Controller bildet die Standortgrenze nicht'
        );
        // Jeder Aufruf reicht sie auch durch.
        assertSame(
            3,
            preg_match_all('/objectScopes\(\)/', $steuerung) - 1,
            'Nicht jeder Aufruf reicht die Grenze durch'
        );
    });

    // ================================================================
    // Fernwartung: kein ausführbares Schema im Verweis
    // ================================================================

    test('Eine Verbindungs-Vorlage darf im Browser keinen Code ausführen', function () {
        // Die mitgelieferten Vorgaben bleiben gültig.
        assertTrue(RemoteAccess::vorlageTauglich('teamviewer10://control?device={id}'), 'TeamViewer wird abgewiesen');
        assertTrue(RemoteAccess::vorlageTauglich('anydesk:{id}'), 'AnyDesk wird abgewiesen');
        assertTrue(RemoteAccess::vorlageTauglich('https://fernwartung.example.org/{id}'), 'https wird abgewiesen');

        // Diese führen im Browser aus, statt ein Programm anzusprechen.
        assertFalse(RemoteAccess::vorlageTauglich('javascript:alert(1)'), 'javascript: gilt als tauglich');
        assertFalse(RemoteAccess::vorlageTauglich('JaVaScRiPt:alert(1)'), 'Groß-/Kleinschreibung hebelt die Prüfung aus');
        assertFalse(RemoteAccess::vorlageTauglich("java\tscript:alert(1)"), 'Ein Steuerzeichen hebelt die Prüfung aus');
        assertFalse(RemoteAccess::vorlageTauglich(' javascript:alert(1)'), 'Führender Abstand hebelt die Prüfung aus');
        assertFalse(RemoteAccess::vorlageTauglich('data:text/html,<script>alert(1)</script>'), 'data: gilt als tauglich');
        assertFalse(RemoteAccess::vorlageTauglich('vbscript:msgbox(1)'), 'vbscript: gilt als tauglich');
        assertFalse(RemoteAccess::vorlageTauglich('file:///etc/passwd'), 'file: gilt als tauglich');
        assertFalse(RemoteAccess::vorlageTauglich('{id}'), 'Eine Vorlage ohne Schema gilt als tauglich');
    });

    test('Eine früher gespeicherte Vorlage wirkt nachträglich nicht', function () use ($wurzel) {
        // Bestandsinstanzen können einen Wert von vor der Prüfung führen;
        // er darf beim Ausgeben nicht mehr greifen.
        $quelle = (string)file_get_contents($wurzel . '/src/Assets/RemoteAccess.php');
        assertTrue(
            (bool)preg_match('/vorlageTauglich\(\$eigene\)\s*\?\s*\$eigene\s*:\s*\'\'/', $quelle),
            'Die Ausgabe prüft die hinterlegte Vorlage nicht'
        );

        $steuerung = (string)file_get_contents($wurzel . '/src/Controllers/AdminAssetSyncController.php');
        assertTrue(
            str_contains($steuerung, 'RemoteAccess::vorlageTauglich($wert)'),
            'Beim Speichern wird nicht geprüft'
        );
    });
};