Security: OAuth-state, Verschlüsselung, Session-Timeout & weitere Härtung

- m365_callback.php: OAuth-state-Validierung gegen Login-CSRF
- includes/Crypto.php: Verschlüsselung-at-rest für UniFi-Passwörter
  (AES-256-GCM/libsodium) mit Klartext-Fallback für Bestandsinstallationen
- install.php: APP_KEY-Generierung + Reinstall nur mit Admin-Session
- Auth.php: absolutes Session-Timeout (SESSION_LIFETIME) durchsetzen
- index.php: CSRF + Throttle auch für anonyme öffentliche Voucher-Erstellung
- UniFiController.php: createVoucher liefert nicht mehr den falschen Code
  bei parallelen Erstellungen (note-Match statt blindes reset())
- display_errors in allen Entry-Points deaktiviert, log_errors aktiviert
- test.php & m365_debug.php hinter requireAdmin() (Info-Leak)
- m365_debug.php: abgeschnittene/kaputte Datei vervollständigt
This commit is contained in:
Claude 2026-06-05 18:45:47 +00:00
parent bf3e55a967
commit 3483da274f
No known key found for this signature in database
17 changed files with 306 additions and 36 deletions

View file

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . '/Crypto.php';
class UniFiController {
private $controllerUrl;
private $username;
@ -169,16 +171,44 @@ class UniFiController {
throw new Exception("Voucher konnte nicht erstellt werden");
}
// Voucher-Code abrufen
// Voucher-Code abrufen. WICHTIG: getVouchers() liefert die Voucher
// unsortiert zurueck ein blindes reset() kann bei parallelen
// Erstellungen den falschen (fremden) Code liefern. Daher gezielt
// nach dem soeben erstellten Voucher suchen: gleiche note + neueste
// create_time.
$vouchers = $this->getVouchers();
if (empty($vouchers)) {
throw new Exception("Voucher-Code konnte nicht abgerufen werden");
}
// Neuesten Voucher zurückgeben
$latestVoucher = reset($vouchers);
$latestVoucher = null;
foreach ($vouchers as $voucher) {
// Nur Voucher mit passender Notiz beruecksichtigen
if (($voucher['note'] ?? null) !== $voucherName) {
continue;
}
if ($latestVoucher === null
|| ($voucher['create_time'] ?? 0) > ($latestVoucher['create_time'] ?? 0)) {
$latestVoucher = $voucher;
}
}
// Fallback: falls keine note-Uebereinstimmung (z.B. Sonderzeichen),
// den global neuesten Voucher nehmen.
if ($latestVoucher === null) {
foreach ($vouchers as $voucher) {
if ($latestVoucher === null
|| ($voucher['create_time'] ?? 0) > ($latestVoucher['create_time'] ?? 0)) {
$latestVoucher = $voucher;
}
}
}
if ($latestVoucher === null || empty($latestVoucher['code'])) {
throw new Exception("Voucher-Code konnte nicht abgerufen werden");
}
return [
'code' => $latestVoucher['code'],
'formatted_code' => $this->formatVoucherCode($latestVoucher['code']),