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
|
|
@ -1,7 +1,9 @@
|
|||
<?php
|
||||
class Auth {
|
||||
private $db;
|
||||
|
||||
/** Pro Request gecachter DB-Datensatz des Session-Users (false = noch nicht geladen) */
|
||||
private $sessionUser = false;
|
||||
|
||||
public function __construct() {
|
||||
try {
|
||||
$this->db = Database::getInstance();
|
||||
|
|
@ -14,7 +16,10 @@ class Auth {
|
|||
ini_set('session.cookie_httponly', 1);
|
||||
ini_set('session.use_strict_mode', 1);
|
||||
ini_set('session.cookie_samesite', 'Lax');
|
||||
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
ini_set('session.cookie_secure', 1);
|
||||
}
|
||||
|
||||
if (!session_start()) {
|
||||
die("Session konnte nicht gestartet werden");
|
||||
}
|
||||
|
|
@ -72,6 +77,8 @@ class Auth {
|
|||
|
||||
private function recordLoginAttempt($ip, $email) {
|
||||
try {
|
||||
// Alte Eintraege aufraeumen, damit die Tabelle nicht unbegrenzt waechst
|
||||
$this->db->query("DELETE FROM login_attempts WHERE attempted_at < DATE_SUB(NOW(), INTERVAL 1 DAY)");
|
||||
$this->db->query(
|
||||
"INSERT INTO login_attempts (ip_address, email) VALUES (?, ?)",
|
||||
[$ip, $email]
|
||||
|
|
@ -138,6 +145,12 @@ class Auth {
|
|||
|
||||
// Session setzen
|
||||
private function setUserSession($user) {
|
||||
// Session-ID nach erfolgreichem Login rotieren (verhindert Session-Fixation)
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
|
||||
$this->sessionUser = false; // User-Cache invalidieren
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['user_email'] = $user['email'];
|
||||
$_SESSION['user_name'] = $user['name'];
|
||||
|
|
@ -160,6 +173,7 @@ class Auth {
|
|||
|
||||
// Ausloggen
|
||||
public function logout() {
|
||||
$this->sessionUser = null;
|
||||
$_SESSION = [];
|
||||
|
||||
if (isset($_COOKIE[session_name()])) {
|
||||
|
|
@ -169,6 +183,25 @@ class Auth {
|
|||
session_destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Laedt den Session-User einmal pro Request aus der DB. Dadurch wirken
|
||||
* Rechteaenderungen (Admin entzogen, Konto deaktiviert/geloescht) sofort
|
||||
* und nicht erst nach Ablauf der Session.
|
||||
*/
|
||||
private function loadSessionUser() {
|
||||
if ($this->sessionUser === false) {
|
||||
$this->sessionUser = null;
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$user = $this->db->fetchOne(
|
||||
"SELECT * FROM users WHERE id = ? AND is_active = 1",
|
||||
[$_SESSION['user_id']]
|
||||
);
|
||||
$this->sessionUser = $user ?: null;
|
||||
}
|
||||
}
|
||||
return $this->sessionUser;
|
||||
}
|
||||
|
||||
// Prüfen ob eingeloggt
|
||||
public function isLoggedIn() {
|
||||
if (!isset($_SESSION['user_id']) || !isset($_SESSION['login_time'])) {
|
||||
|
|
@ -183,24 +216,32 @@ class Auth {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Deaktivierte/geloeschte Konten sofort aussperren
|
||||
if ($this->loadSessionUser() === null) {
|
||||
$this->logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfen ob Admin
|
||||
|
||||
// Prüfen ob Admin (live aus der DB, nicht aus dem Session-Cache)
|
||||
public function isAdmin() {
|
||||
return $this->isLoggedIn() && isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
|
||||
if (!$this->isLoggedIn()) {
|
||||
return false;
|
||||
}
|
||||
$user = $this->loadSessionUser();
|
||||
$isAdmin = $user !== null && (bool)$user['is_admin'];
|
||||
$_SESSION['is_admin'] = $isAdmin;
|
||||
return $isAdmin;
|
||||
}
|
||||
|
||||
|
||||
// Aktuellen Benutzer abrufen
|
||||
public function getCurrentUser() {
|
||||
if (!$this->isLoggedIn()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->db->fetchOne(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
[$_SESSION['user_id']]
|
||||
);
|
||||
return $this->loadSessionUser();
|
||||
}
|
||||
|
||||
// Prüfen ob Benutzer Zugriff auf Site hat
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue