Quelltext des aktuellen Standes
<?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');
});
};