Upload via GUI (360 Dateien)

This commit is contained in:
2026-10-30 21:50:16 +00:00
parent 2ee9be11c0
commit 4e1e6d85f8
5 changed files with 1210 additions and 0 deletions
+261
View File
@@ -0,0 +1,261 @@
<?php
declare(strict_types=1);
/**
* Prüft die Anmelde-Erweiterung für Plugins (Berechtigung auth.login): Eine Anmelde-Adresse wird nur mit der Berechtigung registriert,
* /staff/plugin_login.php leitet nur auf https (oder localhost) weiter, meldet nur aktive Mitarbeiter an, schreibt ins Admin-Protokoll und
* zeigt Fehler auf der Anmeldeseite; der Filter „staff.login.buttons“ liefert Knöpfe nur an Plugins mit der Berechtigung.
* Ein echter Testserver (php -S) mit eigener Datenbank und eigenem Plugin-Ordner.
*/
if (!function_exists('curl_init') || !extension_loaded('pdo_sqlite')) { echo "Anmelde-Test übersprungen: curl oder pdo_sqlite fehlt.\n"; exit(0); }
$work = sys_get_temp_dir() . '/deskhub-pl-' . bin2hex(random_bytes(4));
$pluginDir = $work . '/plugins';
mkdir($pluginDir, 0755, true);
$db = $work . '/db.sqlite';
putenv('DESKHUB_PLUGINS_DIR=' . $pluginDir);
putenv('DESKHUB_TESTING=1');
putenv('DESKHUB_CATALOG_DEFAULTS=' . __DIR__ . '/fixtures/no-catalog.php');
putenv('DESKHUB_SQLITE_PATH=' . $db);
require_once dirname(__DIR__) . '/includes/bootstrap.php';
$failures = [];
$check = function (bool $ok, string $what) use (&$failures): void { if (!$ok) $failures[] = $what; };
$pdo = get_db();
// Zwei Plugins: eines mit Berechtigung auth.login, eines ohne (versucht trotzdem zu registrieren)
$mkZip = function (string $slug, array $perms, string $php) use ($work): string {
$path = $work . "/$slug.zip";
$z = new ZipArchive();
$z->open($path, ZipArchive::CREATE);
$z->addFromString('plugin.json', json_encode(['slug' => $slug, 'name' => $slug, 'version' => '1.0.0', 'entry' => 'plugin.php', 'permissions' => $perms]));
$z->addFromString('plugin.php', $php);
$z->close();
return $path;
};
$handlers = <<<'PHP'
<?php
plugin_register_login('ok', fn($r) => ['staff_id' => (int) plugin_setting('PLUGIN', 'target', '0'), 'via' => 'Testdienst']);
plugin_register_login('go', fn($r) => ['redirect' => (string) ($r['query']['to'] ?? 'https://idp.example.org/auth?x=1')]);
plugin_register_login('nope', fn($r) => ['error' => 'Konto nicht freigegeben <b>x</b>']);
plugin_register_login('boom', function ($r) { throw new RuntimeException('kaputt'); });
plugin_register_login('empty', fn($r) => 'kein array');
plugin_register_login('link', fn($r) => ['local' => '/staff/plugin.php?p=PLUGIN&page=konto', 'notice' => 'Verknüpft: ' . ($r['staff']['email'] ?? '?')], true);
plugin_register_login('linkerr', fn($r) => ['error' => 'Nicht verknüpft', 'back' => '/staff/plugin.php?p=PLUGIN&page=konto'], true);
plugin_register_login('linkevil', fn($r) => ['local' => '//evil.example/x', 'notice' => 'x'], true);
plugin_register_login('linkback', fn($r) => ['error' => 'x', 'back' => 'https://evil.example/'], true);
plugin_register_login('switch', fn($r) => ['staff_id' => 1], true);
plugin_register_login('linkgo', fn($r) => ['redirect' => 'https://idp.example.org/link'], true);
plugin_register_customer_login('cgo', fn($r) => ['redirect' => 'https://idp.example.org/kunde']);
plugin_register_customer_login('cnew', fn($r) => ['customer' => ['email' => (string) ($r['query']['mail'] ?? 'neu@kunde.example'), 'name' => (string) ($r['query']['name'] ?? ''), 'create' => !isset($r['query']['nocreate']), 'via' => 'Testdienst']]);
plugin_register_customer_login('cerr', fn($r) => ['error' => 'Kunde abgelehnt <i>x</i>']);
plugin_register_login('ccb', fn($r) => ['customer' => ['email' => 'rueck@kunde.example', 'name' => 'Rück Kunde', 'create' => true, 'via' => 'Testdienst']]);
plugin_register_login('cerr2', fn($r) => ['audience' => 'customer', 'error' => 'Kundenfehler über die gemeinsame Rückkehr']);
plugin_add_filter('customer.login.buttons', fn($html) => $html . '<a class="btn" href="' . plugin_customer_login_url('PLUGIN', 'cnew') . '">Kunden-Knopf</a>');
plugin_add_filter('staff.login.buttons', fn($html) => $html . '<a class="btn" href="' . plugin_login_url('PLUGIN', 'ok') . '">Testdienst-Knopf</a>');
PHP;
$withPerm = $mkZip('logintest', ['auth.login', 'auth.customer'], str_replace('PLUGIN', 'logintest', $handlers));
$noPerm = $mkZip('loginsneak', [], str_replace('PLUGIN', 'loginsneak', $handlers));
$check(plugin_install_zip($pdo, $withPerm)['ok'] && plugin_install_zip($pdo, $noPerm)['ok'], 'Test-Plugins lassen sich installieren');
$check(plugin_set_active($pdo, 'logintest', true, ['auth.login', 'auth.customer'])['ok'], 'Plugin mit der Berechtigung lässt sich aktivieren');
$check(plugin_set_active($pdo, 'loginsneak', true, [])['ok'], 'Plugin ohne die Berechtigung lässt sich aktivieren');
$check(isset(plugin_known_permissions()['auth.customer']), 'Die Berechtigung auth.customer ist bekannt und beschrieben');
$check(isset(plugin_known_permissions()['auth.login']), 'Die Berechtigung auth.login ist bekannt und beschrieben');
// Mitarbeiter: aktiv und deaktiviert
$ins = $pdo->prepare("INSERT INTO staff (name, email, password_hash, role, is_active, created_at) VALUES (?,?,?,?,?,?)");
$ins->execute(['Anna Aktiv', 'anna@firma.example', password_hash('Geheim-1234567', PASSWORD_DEFAULT), 'agent', 1, db_now()]);
$annaId = (int) $pdo->lastInsertId();
$ins->execute(['Karl Kaputt', 'karl@firma.example', password_hash('Geheim-1234567', PASSWORD_DEFAULT), 'agent', 0, db_now()]);
$karlId = (int) $pdo->lastInsertId();
plugin_set_setting('logintest', 'target', (string) $annaId);
plugin_set_setting('loginsneak', 'target', (string) $annaId);
// Nur das Plugin mit Berechtigung hat Anmelde-Adressen und Filter
$pdo = null;
$port = random_int(34000, 34499);
$env = array_merge(getenv(), ['DESKHUB_PLUGINS_DIR' => $pluginDir, 'DESKHUB_TESTING' => '1', 'DESKHUB_SQLITE_PATH' => $db, 'DESKHUB_CATALOG_DEFAULTS' => __DIR__ . '/fixtures/no-catalog.php']);
$proc = proc_open([PHP_BINARY, '-S', "127.0.0.1:$port", '-t', dirname(__DIR__)], [1 => ['file', $work . '/srv.out', 'w'], 2 => ['file', $work . '/srv.err', 'w']], $pipes, dirname(__DIR__), $env);
for ($i = 0; $i < 60; $i++) { $s = @fsockopen('127.0.0.1', $port, $en, $es, 0.2); if ($s) { fclose($s); break; } usleep(100000); }
$jar = $work . '/jar';
$http = function (string $path, array $opts = []) use ($port, $jar): array {
$c = curl_init("http://127.0.0.1:$port$path");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => 20, CURLOPT_COOKIEJAR => $jar, CURLOPT_COOKIEFILE => $jar]);
$raw = (string) curl_exec($c);
$status = (int) curl_getinfo($c, CURLINFO_RESPONSE_CODE);
$hs = (int) curl_getinfo($c, CURLINFO_HEADER_SIZE);
curl_close($c);
$head = substr($raw, 0, $hs);
return [$status, substr($raw, $hs), preg_match('/^Location:\s*(.+)$/mi', $head, $m) ? trim($m[1]) : ''];
};
$fresh = function () use ($jar): void { @unlink($jar); };
try {
// Anmeldeseite: Knopf nur vom Plugin mit Berechtigung
[$s, $body] = $http('/staff/login.php');
$check($s === 200 && str_contains($body, 'Testdienst-Knopf') && str_contains($body, 'plugin_login.php?p=logintest&e=ok'), 'Anmeldeseite zeigt den Knopf des Plugins mit Berechtigung');
$check(substr_count($body, 'Testdienst-Knopf') === 1, 'Das Plugin ohne Berechtigung bekommt keinen Knopf auf die Anmeldeseite');
// Anmelde-Adressen: ohne Berechtigung nicht registriert
[$s] = $http('/staff/plugin_login.php?p=loginsneak&e=ok');
$check($s === 404, 'Anmelde-Adresse eines Plugins ohne Berechtigung gibt es nicht (404)');
[$s] = $http('/staff/plugin_login.php?p=logintest&e=gibtesnicht');
$check($s === 404, 'Unbekannte Adresse: 404');
[$s] = $http('/staff/plugin_login.php');
$check($s === 404, 'Ohne Angaben: 404');
// Weiterleitung
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=go');
$check($s === 302 && $loc === 'https://idp.example.org/auth?x=1', 'Weiterleitung auf https wird ausgeführt: ' . $loc);
foreach (['javascript:alert(1)', 'http://evil.example/x', 'https://', '//evil.example', 'https://a.example/x y', 'ftp://a.example/'] as $bad) {
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=go&to=' . rawurlencode($bad));
$check($s === 302 && str_ends_with($loc, '/staff/login.php'), 'Ungültiges Weiterleitungsziel wird abgelehnt: ' . $bad . ' -> ' . $loc);
}
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=go&to=' . rawurlencode('http://localhost:9000/auth'));
$check($s === 302 && $loc === 'http://localhost:9000/auth', 'http ist nur zu localhost erlaubt');
// Fehler und Absturz: zurück zur Anmeldeseite mit Meldung (maskiert)
$fresh();
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=nope');
$check($s === 302 && str_ends_with($loc, '/staff/login.php'), 'Fehler führt zurück zur Anmeldeseite');
[$s, $body] = $http('/staff/login.php');
$check(str_contains($body, 'Konto nicht freigegeben &lt;b&gt;x&lt;/b&gt;') && !str_contains($body, '<b>x</b>'), 'Fehlermeldung wird angezeigt und maskiert');
foreach (['boom', 'empty'] as $e) {
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=' . $e);
$check($s === 302 && str_ends_with($loc, '/staff/login.php'), "Plugin-Fehler ($e) führt zurück zur Anmeldeseite");
}
// Anmeldung: nicht angemeldet → Weiterleitung auf die Anmeldeseite; nach Plugin-Anmeldung eingeloggt
$fresh();
[$s, , $loc] = $http('/staff/reports.php');
$check($s === 302 && str_contains($loc, 'login'), 'ohne Anmeldung ist der Mitarbeiterbereich gesperrt');
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=ok');
$check($s === 302 && str_ends_with($loc, '/staff/reports.php'), 'Geprüfter Mitarbeiter wird angemeldet und weitergeleitet: ' . $loc);
[$s, $body] = $http('/staff/reports.php');
$check($s === 200 && !str_contains($body, 'name="password"'), 'danach ist der Mitarbeiterbereich offen');
$log = (string) get_db()->query("SELECT GROUP_CONCAT(action, ' | ') FROM admin_audit_log WHERE action LIKE 'Anmeldung über%'")->fetchColumn();
$check(str_contains($log, 'Anmeldung über Testdienst (Plugin logintest)'), 'Die Anmeldung steht im Admin-Protokoll: ' . $log);
// Schon angemeldet: normale Anmelde-Adressen führen nur zurück in den Bereich; Adressen mit „whenLoggedIn“ laufen
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=ok');
$check($s === 302 && str_ends_with($loc, '/staff/reports.php'), 'Angemeldet: eine normale Anmelde-Adresse meldet nicht noch einmal an');
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=go');
$check($s === 302 && str_ends_with($loc, '/staff/reports.php'), 'Angemeldet: auch eine Weiterleitungs-Adresse ohne Freigabe wird ignoriert');
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=linkgo');
$check($s === 302 && $loc === 'https://idp.example.org/link', 'Angemeldet: freigegebene Adresse darf weiter zum Anmeldedienst leiten');
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=link');
$check($s === 302 && str_ends_with($loc, '/staff/plugin.php?p=logintest&page=konto'), 'Angemeldet: „local“ führt zurück in den Mitarbeiterbereich: ' . $loc);
[$s, $body] = $http('/staff/reports.php');
$check(str_contains($body, 'Verknüpft: anna@firma.example'), 'Der Hinweis erscheint und das Plugin kennt den angemeldeten Mitarbeiter');
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=linkevil');
$check($s === 302 && str_ends_with($loc, '/staff/reports.php') && !str_contains($loc, 'evil'), 'Ein ungültiges lokales Ziel (//evil) wird nicht angenommen: ' . $loc);
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=linkerr');
$check($s === 302 && str_ends_with($loc, '/staff/plugin.php?p=logintest&page=konto'), 'Fehler mit „back“ führt zur angegebenen Seite zurück: ' . $loc);
[$s, $body] = $http('/staff/reports.php');
$check(str_contains($body, 'Nicht verknüpft'), 'Die Fehlermeldung wird angezeigt');
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=linkback');
$check($s === 302 && str_ends_with($loc, '/staff/reports.php'), 'Ein fremdes „back“-Ziel wird nicht angenommen: ' . $loc);
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=switch');
$check($s === 302 && str_ends_with($loc, '/staff/reports.php'), 'Angemeldet kann nie ein anderer Mitarbeiter angemeldet werden');
[$s, $body] = $http('/staff/reports.php');
$check(str_contains($body, 'keinen anderen Mitarbeiter') && str_contains($body, 'Anna Aktiv') || str_contains($body, 'anna@firma.example') || $s === 200, 'Danach bleibt Anna angemeldet');
// Deaktivierter Mitarbeiter wird nie angemeldet
$fresh();
plugin_set_setting('logintest', 'target', (string) $karlId);
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=ok');
$check($s === 302 && str_ends_with($loc, '/staff/login.php'), 'Deaktivierter Mitarbeiter kommt nicht hinein');
[$s] = $http('/staff/reports.php');
$check($s === 302, 'und bleibt ausgesperrt');
plugin_set_setting('logintest', 'target', '999999');
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=ok');
$check($s === 302 && str_ends_with($loc, '/staff/login.php'), 'Unbekannte Mitarbeiter-ID wird abgelehnt');
// ── Kunden ──
$fresh();
[$s, $body] = $http('/login.php');
$check($s === 200 && str_contains($body, 'Kunden-Knopf') && str_contains($body, '/plugin_login.php?p=logintest&e=cnew') && substr_count($body, 'Kunden-Knopf') === 1, 'Kunden-Anmeldeseite zeigt nur den Knopf des Plugins mit auth.customer');
$check(!str_contains($body, 'Testdienst-Knopf'), 'Mitarbeiter-Knöpfe erscheinen dort nicht');
[$s, $body] = $http('/staff/login.php');
$check(!str_contains($body, 'Kunden-Knopf'), 'Und Kunden-Knöpfe nicht bei den Mitarbeitern');
[$s] = $http('/plugin_login.php?p=loginsneak&e=cnew');
$check($s === 404, 'Kunden-Adresse eines Plugins ohne auth.customer gibt es nicht');
[$s] = $http('/plugin_login.php?p=logintest&e=ok');
$check($s === 404, 'Eine Mitarbeiter-Adresse ist über den Kunden-Einstieg nicht erreichbar');
[$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cgo');
$check($s === 302 && $loc === 'https://idp.example.org/kunde', 'Kunden-Weiterleitung auf https wird ausgeführt');
// Neues Konto wird angelegt und angemeldet
[$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cnew&mail=' . rawurlencode('Neu.Kunde@Kunde.Example') . '&name=' . rawurlencode('<b>Nora</b> Neu'));
$check($s === 302 && str_ends_with($loc, '/home.php'), 'Neuer Kunde wird angemeldet: ' . $loc);
$u = get_db()->query("SELECT * FROM users WHERE email = 'neu.kunde@kunde.example'")->fetch();
$check($u && $u['name'] === 'Nora Neu' && strlen((string) $u['password_hash']) > 20, 'Kundenkonto wurde angelegt (E-Mail klein geschrieben, Name ohne HTML, Passwort zufällig)');
[$s, $body] = $http('/home.php');
$check(str_contains($body, 'Ihr Konto wurde erstellt'), 'Kunde sieht den Willkommens-Hinweis');
$log = (string) get_db()->query("SELECT GROUP_CONCAT(action, ' | ') FROM admin_audit_log WHERE action LIKE 'Kundenkonto über%'")->fetchColumn();
$check(str_contains($log, 'Kundenkonto über Testdienst angelegt: neu.kunde@kunde.example'), 'Das Anlegen steht im Admin-Protokoll: ' . $log);
// Zweites Mal: vorhandenes Konto, kein zweites
$fresh();
$http('/plugin_login.php?p=logintest&e=cnew&mail=' . rawurlencode('neu.kunde@kunde.example'));
$check((int) get_db()->query("SELECT COUNT(*) FROM users WHERE email = 'neu.kunde@kunde.example'")->fetchColumn() === 1, 'Ein vorhandenes Konto wird nicht doppelt angelegt');
// Ohne Erlaubnis zum Anlegen
$fresh();
[$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cnew&nocreate=1&mail=' . rawurlencode('unbekannt@kunde.example'));
$check($s === 302 && str_ends_with($loc, '/login.php') && (int) get_db()->query("SELECT COUNT(*) FROM users WHERE email = 'unbekannt@kunde.example'")->fetchColumn() === 0, 'Ohne „create“ wird kein Konto angelegt');
[$s, $body] = $http('/login.php');
$check(str_contains($body, 'kein Kundenkonto'), 'und die Meldung erscheint');
// Ungültige und gesperrte Adressen
foreach (['kein-mail', 'a@b', str_repeat('x', 160) . '@kunde.example'] as $bad) {
[$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cnew&mail=' . rawurlencode($bad));
$check($s === 302 && str_ends_with($loc, '/login.php'), 'Ungültige E-Mail-Adresse wird abgelehnt: ' . substr($bad, 0, 20));
}
get_db()->prepare("INSERT INTO banlist (match_type, value, reason, created_at) VALUES ('email', ?, 'Test', ?)")->execute(['gesperrt@kunde.example', db_now()]);
[$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cnew&mail=' . rawurlencode('gesperrt@kunde.example'));
$check($s === 302 && str_ends_with($loc, '/login.php') && (int) get_db()->query("SELECT COUNT(*) FROM users WHERE email = 'gesperrt@kunde.example'")->fetchColumn() === 0, 'Eine gesperrte E-Mail-Adresse kommt nicht hinein und bekommt kein Konto');
// Fehler (maskiert)
[$s, , $loc] = $http('/plugin_login.php?p=logintest&e=cerr');
[$s, $body] = $http('/login.php');
$check(str_contains($body, 'Kunde abgelehnt &lt;i&gt;x&lt;/i&gt;') && !str_contains($body, '<i>x</i>'), 'Fehlermeldung für Kunden wird maskiert angezeigt');
// Rückkehr über die gemeinsame Adresse (staff/plugin_login.php)
$fresh();
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=ccb');
$check($s === 302 && str_ends_with($loc, '/home.php') && (int) get_db()->query("SELECT COUNT(*) FROM users WHERE email = 'rueck@kunde.example'")->fetchColumn() === 1, 'Die gemeinsame Rückkehr-Adresse kann eine Kunden-Anmeldung beenden und legt das Konto an');
[$s, $body] = $http('/staff/reports.php');
$check($s === 302, 'und macht dabei niemanden zum Mitarbeiter');
$fresh();
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=cerr2');
$check($s === 302 && str_ends_with($loc, '/login.php') && !str_contains($loc, 'staff'), 'Kunden-Fehler über die gemeinsame Adresse führen auf die Kunden-Anmeldeseite');
// Plugin ohne auth.customer darf keine Kunden anmelden, auch nicht über die gemeinsame Adresse
$fresh();
// (loginsneak hat keine Mitarbeiter-Adressen registriert, daher gibt es hier nichts zu erreichen: 404 ist die richtige Antwort)
[$s] = $http('/staff/plugin_login.php?p=loginsneak&e=ccb');
$check($s === 404, 'Ein Plugin ohne die Berechtigungen kommt nicht einmal bis zum Ergebnis');
$st0 = &plugin_state();
$st0['booted'] = false;
plugins_boot();
$check(!in_array('auth.customer', plugin_state()['perms']['loginsneak'] ?? [], true) && in_array('auth.customer', plugin_state()['perms']['logintest'] ?? [], true), 'Nur das Plugin mit Freigabe hat auth.customer in seinen Rechten');
// Begrenzung
$fresh();
$limited = false;
for ($i = 0; $i < 60 && !$limited; $i++) {
[$s, , $loc] = $http('/staff/plugin_login.php?p=logintest&e=go');
if (str_ends_with($loc, '/staff/login.php')) $limited = true;
}
$check($limited, 'Zu viele Aufrufe werden begrenzt');
} finally {
proc_terminate($proc);
proc_close($proc);
}
usleep(300000);
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($work, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $f) {
$f->isDir() ? @rmdir($f->getPathname()) : @unlink($f->getPathname());
}
@rmdir($work);
if ($failures) {
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
exit(1);
}
echo "Plugin login checks passed.\n";