Security-, Bugfix- und UX-Überarbeitung auf Basis des Code-Reviews
Sicherheit: - Bulk-Erstellung serverseitig auf eingeloggte Nutzer beschränkt; expire_minutes wird validiert (anonym: nur Default/Template-Werte, eingeloggt: max. 1 Jahr) - IP-basiertes Rate-Limit über neue Tabelle request_throttle (Voucher-Erstellung + Passwort-Reset-Anfragen), Session-Fallback für Alt-Installationen; Migration 0002 - session_regenerate_id() nach Login, Secure-Cookie-Flag bei HTTPS - Admin-/Aktiv-Status wird pro Request live aus der DB geprüft (Rechteentzug & Deaktivierung wirken sofort); Schutz vor Selbst-Degradierung im Benutzer-Edit - Alle state-ändernden Admin-Aktionen von GET auf POST umgestellt (kein CSRF-Token mehr in URLs) - login_simple.php (Legacy, Debug-Leak) entfernt; cron_test.php nur noch für Admins; .htaccess auf Apache-2.4-Syntax inkl. cron_test.php - M365 Client Secret wird nicht mehr ins Formular zurückgegeben - Updater: Zip-Slip-/Pfad-Traversal-Schutz, Backup vor dem Anwenden mit automatischem Rollback bei Fehlern, AuditLogger-Bug behoben - cron_sync: Token-Vergleich mit hash_equals; login_attempts-Pruning - CSV-Export gegen Excel-Formula-Injection abgesichert Bugfixes: - M365-Login: Fallback auf userPrincipalName, wenn Graph kein 'mail' liefert (Nutzer ohne Exchange-Postfach konnten sich nie anmelden) - PRG-Pattern überall: F5 erzeugt keine Duplikat-Voucher und wiederholt keine Admin-Aktionen (Session-Flash-Messages) - QR-Code nicht mehr invertiert (schwarz auf weiß, scanbar) - Bulk-Erstellung nutzt den UniFi 'n'-Parameter: 1 API-Call statt n× Login + Voucherlisten-Abruf; exaktes Code-Matching per create_time statt "global neuester Voucher" - Mailer: doppelte Zeilenumbrüche behoben, AUTH nur mit Credentials, SMTP-Dot-Stuffing, CLI-sicherer EHLO-Host - forgot_password: System-URL-Auto-Detect (Reset-Link war sonst relativ/kaputt) + Rate-Limit - Audit-Log-Labels an tatsächliche Action-Keys angepasst; Voucher-Erstellung (einzeln & bulk) wird jetzt auditiert - Site-Edit testet die Verbindung auch ohne Passwortänderung UX/UI: - Alert-/Badge-Styles zentral in global.css mit Dark-Mode-Variablen (vorher 7× dupliziert mit hart codierten Hellfarben) - Sticky-Formulare + Tab-Erhalt nach Validierungsfehlern (Bulk), Settings kehren nach dem Speichern zum aktiven Tab zurück - Gültigkeit menschenlesbar (z.B. "8 Stunden" statt "480 Minuten") - Voucher-Name-Default "Gast/Guest" im öffentlichen Modus - Favicon auch auf Login-/öffentlichen Seiten - Verbindungstest-Button pro Site-Karte (Health-Check) - i18n-Pass: Confirm-Dialoge, Toasts, Fehl-/Erfolgsmeldungen in de/en - Sprachumschalter ohne fetch+reload (kein Re-Submit-Dialog) - A11y: Esc schließt Modals, aria-live für Toasts, aria-labels auf Icon-Buttons; APP_KEY-Warnbanner im Dashboard - Dashboard-Sync: set_time_limit passend zur Site-Anzahl; Voucher-Sync mit Map statt SELECT pro Voucher Tooling: - GitHub-Actions-Workflow: PHP-Lint aller Dateien + de/en-Key-Parität https://claude.ai/code/session_01KKVpVPJjrTKGoRgpJcySD4
This commit is contained in:
parent
f747a3d429
commit
6e19958a37
31 changed files with 1040 additions and 628 deletions
|
|
@ -8,6 +8,7 @@ require_once __DIR__ . '/includes/Database.php';
|
|||
require_once __DIR__ . '/includes/Auth.php';
|
||||
require_once __DIR__ . '/includes/Mailer.php';
|
||||
require_once __DIR__ . '/includes/I18n.php';
|
||||
require_once __DIR__ . '/includes/Helpers.php';
|
||||
|
||||
$auth = new Auth();
|
||||
if ($auth->isLoggedIn()) { header('Location: index.php'); exit; }
|
||||
|
|
@ -16,8 +17,18 @@ I18n::init();
|
|||
$db = Database::getInstance();
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
$logoUrl = $db->getSetting('logo_url', '');
|
||||
$faviconUrl = $db->getSetting('favicon_url', '');
|
||||
$systemUrl = rtrim($db->getSetting('system_url', ''), '/');
|
||||
|
||||
// Fallback: URL automatisch erkennen (wie im Mailer), sonst ist der
|
||||
// Reset-Link in der E-Mail relativ und damit kaputt.
|
||||
if (empty($systemUrl)) {
|
||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||
$scriptPath = dirname($_SERVER['SCRIPT_NAME']);
|
||||
$scriptPath = $scriptPath === '/' ? '' : $scriptPath;
|
||||
$systemUrl = $protocol . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . $scriptPath;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
|
|
@ -27,7 +38,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$error = __('error_email_invalid');
|
||||
} else {
|
||||
$user = $db->fetchOne("SELECT * FROM users WHERE email = ? AND is_active = 1 AND password_hash IS NOT NULL", [$email]);
|
||||
// IP-Rate-Limit gegen Mail-Bombing: max. 5 Reset-Anfragen / 15 Min.
|
||||
// Bei Limit trotzdem die generische Erfolgsmeldung zeigen (keine
|
||||
// Information darueber preisgeben, ob das Konto existiert).
|
||||
$resetLimited = throttleHit($db, 'password_reset', 5, 15) === true;
|
||||
|
||||
$user = $resetLimited
|
||||
? null
|
||||
: $db->fetchOne("SELECT * FROM users WHERE email = ? AND is_active = 1 AND password_hash IS NOT NULL", [$email]);
|
||||
|
||||
// Always show success (don't reveal whether email exists)
|
||||
if ($user) {
|
||||
|
|
@ -76,6 +94,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= __('reset_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||
<?php if ($faviconUrl): ?>
|
||||
<link rel="icon" href="<?= htmlspecialchars($faviconUrl) ?>">
|
||||
<?php endif; ?>
|
||||
<link rel="stylesheet" href="assets/global.css">
|
||||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||||
<style>
|
||||
|
|
@ -91,9 +112,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
input:focus { outline: none; border-color: var(--accent); }
|
||||
.btn { width: 100%; padding: 14px; background: var(--accent); color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; margin-top: 8px; }
|
||||
.btn:hover { background: var(--accent-hover); transform: translateY(-2px); }
|
||||
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; text-align: left; }
|
||||
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
|
||||
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
|
||||
.back-link { display: block; margin-top: 22px; color: var(--accent); text-decoration: none; font-size: 14px; }
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue