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,7 +1,8 @@
<?php
// Error Reporting für Debugging
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/includes/Database.php';
@ -13,6 +14,32 @@ $auth = new Auth();
$db = Database::getInstance();
$mailer = new Mailer();
/**
* Session-basierter Throttle fuer die anonyme oeffentliche Voucher-Erstellung.
* Erlaubt max. 10 Erstellungen in 10 Minuten pro Session. Verhindert, dass
* der oeffentliche Modus zum Spammen des UniFi-Controllers missbraucht wird.
*/
function isVoucherRateLimited() {
$window = 600; // 10 Minuten
$maxRequests = 10;
$now = time();
$timestamps = $_SESSION['voucher_create_times'] ?? [];
// Nur Eintraege innerhalb des Zeitfensters behalten
$timestamps = array_values(array_filter($timestamps, function ($t) use ($now, $window) {
return ($now - $t) < $window;
}));
if (count($timestamps) >= $maxRequests) {
$_SESSION['voucher_create_times'] = $timestamps;
return true;
}
$timestamps[] = $now;
$_SESSION['voucher_create_times'] = $timestamps;
return false;
}
// Settings laden
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
$logoUrl = $db->getSetting('logo_url', '');
@ -67,8 +94,14 @@ if ($auth->isLoggedIn()) {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) {
if (!$publicAccess && !$auth->isLoggedIn()) {
$error = 'Sie müssen angemeldet sein';
} elseif ($auth->isLoggedIn() && !$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
} elseif (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
// CSRF wird jetzt fuer ALLE geprueft auch fuer anonyme oeffentliche
// Erstellung (Token wird per Session auch ohne Login vergeben).
$error = 'Ungültiges Sicherheits-Token';
} elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) {
// Einfacher Session-basierter Throttle gegen Missbrauch/Spam im
// oeffentlichen Modus (kein Login = kein Benutzerkontext).
$error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.';
} else {
try {
$siteId = (int)($_POST['site_id'] ?? 0);
@ -114,7 +147,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) {
$controller = new UniFiController(
$site['unifi_controller_url'],
$site['unifi_username'],
$site['unifi_password'],
Crypto::decrypt($site['unifi_password']),
$site['site_id']
);
@ -558,9 +591,8 @@ $autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0;
<!-- FIX: Trigger-Feld kommt nicht mehr vom Submit-Button -->
<input type="hidden" name="create_voucher" value="1">
<?php if ($auth->isLoggedIn()): ?>
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<?php endif; ?>
<!-- CSRF-Token fuer alle (auch anonyme oeffentliche Erstellung) -->
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($auth->getCsrfToken()) ?>">
<div class="form-group">
<label for="voucher_name">Voucher-Name *</label>