Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
use OpenNIT\Tickets\TicketManager;
/**
* Deckt die Härtungsmaßnahmen ab, die aus dem Sicherheits-Review hervorgingen.
*
* Geprüft wird jeweils die Entscheidung, nicht das Drumherum: welche Endung ein
* Anhang bekommt, welche Rate-Limit-Muster überhaupt greifen und ob die
* Kiosk-Freigabe im Auslieferungszustand geschlossen ist.
*/
return static function (): void {
/** Ruft die private Endungs-Ermittlung des TicketManagers auf. */
$endung = static function (string $dateiname): string {
$m = new ReflectionMethod(TicketManager::class, 'safeAttachmentExtension');
$m->setAccessible(true);
return (string)$m->invoke(null, $dateiname);
};
// ----------------------------------------------------------------
// Anhänge
// ----------------------------------------------------------------
test('Ausführbare Endungen werden neutralisiert', function () use ($endung) {
foreach (['schaden.php', 'x.phtml', 'y.phar', 'z.PHP5', 'a.htaccess', 'b.sh'] as $name) {
assertSame('bin', $endung($name), $name);
}
});
test('Doppelte Endungen zählen nur mit ihrem letzten Teil', function () use ($endung) {
// pathinfo liefert die letzte Endung – „bild.php.jpg" ist ein JPG.
assertSame('jpg', $endung('bild.php.jpg'));
// Umgekehrt greift die Neutralisierung.
assertSame('bin', $endung('bild.jpg.php'));
});
test('Gewöhnliche Anhänge behalten ihre Endung', function () use ($endung) {
assertSame('pdf', $endung('Angebot.pdf'));
assertSame('docx', $endung('Vertrag.docx'));
assertSame('png', $endung('Screenshot.PNG'));
});
test('Fehlende oder ungewöhnliche Endungen ergeben keinen Pfadbestandteil', function () use ($endung) {
assertSame('', $endung('Datei ohne Endung'));
// Sonderzeichen werden entfernt, damit kein Pfad- oder Nullbyte-Trick bleibt.
assertSame('jpg', $endung("bild.j\0pg"));
assertSame('', $endung('archiv.'));
});
// ----------------------------------------------------------------
// Rate-Limit-Muster
// ----------------------------------------------------------------
test('Die konfigurierten Muster treffen die tatsächlichen Pfade', function () {
$cfg = require dirname(__DIR__) . '/config/security.php';
$muster = array_keys($cfg['rate_limit']['endpoints'] ?? []);
// Muster greifen über Präfix oder Enthaltensein (siehe RateLimiter::pathMatches).
$trifft = static function (string $pfad) use ($muster): bool {
foreach ($muster as $m) {
if (str_starts_with($pfad, '/' . $m) || str_contains($pfad, $m)) {
return true;
}
}
return false;
};
foreach (['/login', '/auth/ldap', '/auth/mfa', '/password/forgot', '/password/reset',
'/upload/image', '/files/upload'] as $pfad) {
assertTrue($trifft($pfad), 'Kein Limit greift für ' . $pfad);
}
});
test('Die Auslieferung statischer Dateien wird nicht gedrosselt', function () {
$cfg = require dirname(__DIR__) . '/config/security.php';
$muster = array_keys($cfg['rate_limit']['endpoints'] ?? []);
$trifft = static function (string $pfad) use ($muster): bool {
foreach ($muster as $m) {
if (str_starts_with($pfad, '/' . $m) || str_contains($pfad, $m)) {
return true;
}
}
return false;
};
// Bilder und Anhänge werden häufig geladen und dürfen kein Limit auslösen.
assertFalse($trifft('/uploads/logo/logo_1.png'), '/uploads/... darf nicht greifen');
assertFalse($trifft('/assets/css/app.css'), '/assets/... darf nicht greifen');
});
// ----------------------------------------------------------------
// Signatur-Kiosk
// ----------------------------------------------------------------
test('Ohne freigegebene Adresse bleibt der Kiosk geschlossen', function () {
$quelle = (string)file_get_contents(
dirname(__DIR__) . '/src/Controllers/SignatureController.php'
);
// Die frühere Freigabe bei leerer Liste darf nicht zurückkehren.
assertFalse(
str_contains($quelle, 'return true; // keine Allowlist'),
'Leere Allowlist darf keinen Zugang mehr gewähren'
);
assertTrue(
str_contains($quelle, 'private function kioskIpAllowed'),
'Getrennte IP-Prüfung erwartet'
);
});
// ----------------------------------------------------------------
// CSRF-Ausnahmen
// ----------------------------------------------------------------
test('Die CSRF-Ausnahmen decken genau die tokengesicherten Pfade ab', function () {
$quelle = (string)file_get_contents(dirname(__DIR__) . '/src/Core/Application.php');
// Der Monitoring-Webhook wird von externen Alarmquellen ohne Sitzung
// aufgerufen; ohne Ausnahme greift die globale Prüfung vor dem Controller.
assertTrue(
str_contains($quelle, "'/monitoring/webhook',"),
'Ausnahme für den Webhook fehlt'
);
assertTrue(
str_contains($quelle, "'/monitoring/webhook/',"),
'Ausnahme für die Quellen-Variante fehlt'
);
// Die Ausnahme darf nicht auf die Verwaltungsseiten des Moduls durchschlagen.
$praefix = '/monitoring/webhook/';
assertFalse(str_starts_with('/monitoring/settings', $praefix));
assertFalse(str_starts_with('/monitoring/webhook-settings', $praefix));
assertTrue(str_starts_with('/monitoring/webhook/zabbix', $praefix));
});
// ----------------------------------------------------------------
// Vorschau geteilter Dateien
// ----------------------------------------------------------------
test('Die Vorschau leitet den Medientyp aus der Endung ab', function () {
$klasse = new ReflectionClass(\OpenNIT\Controllers\ShareController::class);
$methode = $klasse->getMethod('inlinePreviewMime');
$methode->setAccessible(true);
$ctrl = $klasse->newInstanceWithoutConstructor();
$mime = static fn(string $name) => $methode->invoke($ctrl, $name);
assertSame('image/png', $mime('bild.png'));
assertSame('application/pdf', $mime('vertrag.PDF'));
// Eine umbenannte Datei erhält den Typ ihrer neuen Endung, nicht den
// beim Hochladen gespeicherten – und alles Übrige gar keine Vorschau.
assertSame(null, $mime('daten.xml'));
assertSame(null, $mime('seite.html'));
assertSame(null, $mime('zeichnung.svg'));
assertSame(null, $mime('ohne-endung'));
});
};