['max_attempts' => 5, 'window' => 300], // 5 Versuche in 5 Minuten 'api' => ['max_attempts' => 100, 'window' => 3600], // 100 Requests pro Stunde 'register' => ['max_attempts' => 3, 'window' => 1800], // 3 Registrierungen in 30 Minuten 'password_reset' => ['max_attempts' => 3, 'window' => 3600], // 3 Reset-Versuche pro Stunde 'review' => ['max_attempts' => 10, 'window' => 3600], // 10 Bewertungen pro Stunde 'contact' => ['max_attempts' => 5, 'window' => 1800] // 5 Kontaktformulare in 30 Minuten ]; // CSRF Token Konfiguration private $csrfTokenName = 'webshop_csrf_token'; private $csrfTokenExpiry = 3600; // 1 Stunde // Input Sanitization Regeln private $sanitizationRules = [ 'string' => FILTER_SANITIZE_STRING, 'email' => FILTER_SANITIZE_EMAIL, 'url' => FILTER_SANITIZE_URL, 'int' => FILTER_SANITIZE_NUMBER_INT, 'float' => FILTER_SANITIZE_NUMBER_FLOAT, 'special_chars' => FILTER_SANITIZE_SPECIAL_CHARS ]; public function __construct() { $this->conn = DriverManager::getConnection([ 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' ]); $this->session = new SessionHandler(); $this->config = new Configuration(); } /** * CSRF Token generieren */ public function generateCSRFToken($action = 'default') { $token = bin2hex(random_bytes(32)); $expiry = time() + $this->csrfTokenExpiry; $_SESSION[$this->csrfTokenName . '_' . $action] = [ 'token' => $token, 'expiry' => $expiry ]; return $token; } /** * CSRF Token validieren */ public function validateCSRFToken($token, $action = 'default') { if (!isset($_SESSION[$this->csrfTokenName . '_' . $action])) { return false; } $stored = $_SESSION[$this->csrfTokenName . '_' . $action]; if (time() > $stored['expiry']) { unset($_SESSION[$this->csrfTokenName . '_' . $action]); return false; } if (!hash_equals($stored['token'], $token)) { return false; } // Token nach erfolgreicher Validierung löschen (One-Time-Use) unset($_SESSION[$this->csrfTokenName . '_' . $action]); return true; } /** * Rate Limiting prüfen */ public function checkRateLimit($action, $identifier = null) { if (!isset($this->rateLimits[$action])) { return true; // Keine Limits für unbekannte Aktionen } $limit = $this->rateLimits[$action]; $identifier = $identifier ?: $this->getClientIdentifier(); try { // Prüfe bestehende Einträge $stmt = $this->conn->prepare(' SELECT COUNT(*) as attempts FROM ws_rate_limit WHERE action = ? AND identifier = ? AND created_at > DATE_SUB(NOW(), INTERVAL ? SECOND) '); $stmt->execute([$action, $identifier, $limit['window']]); $result = $stmt->fetchAssociative(); if ($result['attempts'] >= $limit['max_attempts']) { return false; } // Neuen Eintrag hinzufügen $stmt = $this->conn->prepare(' INSERT INTO ws_rate_limit (action, identifier, created_at) VALUES (?, ?, NOW()) '); $stmt->execute([$action, $identifier]); return true; } catch (Exception $e) { error_log('Rate limiting error: ' . $e->getMessage()); return true; // Bei Fehlern erlauben } } /** * Input validieren und sanitieren */ public function sanitizeInput($data, $rules = []) { if (is_array($data)) { $sanitized = []; foreach ($data as $key => $value) { $rule = $rules[$key] ?? 'string'; $sanitized[$key] = $this->sanitizeValue($value, $rule); } return $sanitized; } return $this->sanitizeValue($data, $rules); } /** * Einzelnen Wert sanitieren */ private function sanitizeValue($value, $rule = 'string') { if (is_null($value)) { return null; } switch ($rule) { case 'email': return filter_var($value, FILTER_SANITIZE_EMAIL); case 'url': return filter_var($value, FILTER_SANITIZE_URL); case 'int': return filter_var($value, FILTER_SANITIZE_NUMBER_INT); case 'float': return filter_var($value, FILTER_SANITIZE_NUMBER_FLOAT); case 'special_chars': return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); case 'string': default: return strip_tags(trim($value)); } } /** * SQL Injection Prevention */ public function validateSQLInput($value, $type = 'string') { if (is_null($value)) { return null; } // Entferne gefährliche Zeichen $dangerous = [';', '--', '/*', '*/', 'xp_', 'sp_', 'exec', 'union', 'select', 'insert', 'update', 'delete', 'drop', 'create']; $value = strtolower($value); foreach ($dangerous as $danger) { if (strpos($value, $danger) !== false) { throw new \Exception('Invalid input detected'); } } return $this->sanitizeValue($value, $type); } /** * XSS Prevention */ public function preventXSS($data) { if (is_array($data)) { return array_map([$this, 'preventXSS'], $data); } return htmlspecialchars($data, ENT_QUOTES, 'UTF-8'); } /** * Passwort-Hashing */ public function hashPassword($password) { return password_hash($password, PASSWORD_ARGON2ID, [ 'memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3 ]); } /** * Passwort verifizieren */ public function verifyPassword($password, $hash) { return password_verify($password, $hash); } /** * Sichere Session-Konfiguration */ public function configureSecureSession() { // HTTPS erzwingen if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') { if (!headers_sent()) { header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']); exit(); } } // Sichere Session-Einstellungen ini_set('session.cookie_httponly', 1); ini_set('session.cookie_secure', 1); ini_set('session.use_strict_mode', 1); ini_set('session.cookie_samesite', 'Strict'); // Session-Regeneration if (!isset($_SESSION['last_regeneration'])) { session_regenerate_id(true); $_SESSION['last_regeneration'] = time(); } elseif (time() - $_SESSION['last_regeneration'] > 300) { // Alle 5 Minuten session_regenerate_id(true); $_SESSION['last_regeneration'] = time(); } } /** * SSL/TLS Konfiguration prüfen */ public function checkSSLConfiguration() { $issues = []; // HTTPS prüfen if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') { $issues[] = 'HTTPS nicht aktiviert'; } // Security Headers prüfen $headers = [ 'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains', 'X-Content-Type-Options' => 'nosniff', 'X-Frame-Options' => 'DENY', 'X-XSS-Protection' => '1; mode=block', 'Referrer-Policy' => 'strict-origin-when-cross-origin', 'Content-Security-Policy' => "default-src 'self'; script-src 'self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com; img-src 'self' data: https:; font-src 'self' cdn.jsdelivr.net cdnjs.cloudflare.com;" ]; foreach ($headers as $header => $value) { if (!headers_sent()) { header("$header: $value"); } } return $issues; } /** * Client-Identifier generieren */ private function getClientIdentifier() { $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'; return hash('sha256', $ip . $userAgent); } /** * Audit-Log erstellen */ public function logSecurityEvent($event, $details = [], $level = 'info') { try { $stmt = $this->conn->prepare(' INSERT INTO ws_security_log (event, details, level, ip_address, user_agent, created_at) VALUES (?, ?, ?, ?, ?, NOW()) '); $stmt->execute([ $event, json_encode($details), $level, $_SERVER['REMOTE_ADDR'] ?? 'unknown', $_SERVER['HTTP_USER_AGENT'] ?? 'unknown' ]); } catch (Exception $e) { error_log('Security log error: ' . $e->getMessage()); } } /** * Brute-Force-Schutz */ public function checkBruteForce($action, $identifier = null) { $identifier = $identifier ?: $this->getClientIdentifier(); try { $stmt = $this->conn->prepare(' SELECT COUNT(*) as attempts, MAX(created_at) as last_attempt FROM ws_security_log WHERE event = ? AND identifier = ? AND level = 'error' AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR) '); $stmt->execute([$action, $identifier]); $result = $stmt->fetchAssociative(); if ($result['attempts'] > 10) { // Mehr als 10 Fehler in 1 Stunde return false; } return true; } catch (Exception $e) { error_log('Brute force check error: ' . $e->getMessage()); return true; } } /** * Captcha-Validierung */ public function validateCaptcha($response, $secret = null) { if (!$secret) { $secret = getenv('RECAPTCHA_SECRET_KEY'); } if (!$secret) { return true; // Kein reCAPTCHA konfiguriert } $url = 'https://www.google.com/recaptcha/api/siteverify'; $data = [ 'secret' => $secret, 'response' => $response, 'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '' ]; $options = [ 'http' => [ 'header' => "Content-type: application/x-www-form-urlencoded\r\n", 'method' => 'POST', 'content' => http_build_query($data) ] ]; $context = stream_context_create($options); $result = file_get_contents($url, false, $context); $response = json_decode($result, true); return $response['success'] ?? false; } /** * Zwei-Faktor-Authentifizierung */ public function generateTOTP($secret = null) { if (!$secret) { $secret = bin2hex(random_bytes(32)); } $timeSlice = floor(time() / 30); $hash = hash_hmac('sha1', $timeSlice, $secret, true); $offset = ord($hash[19]) & 0xf; $code = ( ((ord($hash[$offset]) & 0x7f) << 24) | ((ord($hash[$offset + 1]) & 0xff) << 16) | ((ord($hash[$offset + 2]) & 0xff) << 8) | (ord($hash[$offset + 3]) & 0xff) ) % 1000000; return str_pad($code, 6, '0', STR_PAD_LEFT); } /** * TOTP validieren */ public function validateTOTP($code, $secret, $window = 1) { $timeSlice = floor(time() / 30); for ($i = -$window; $i <= $window; $i++) { $expectedCode = $this->generateTOTP($secret); if ($code === $expectedCode) { return true; } } return false; } /** * Datei-Upload-Sicherheit */ public function validateFileUpload($file, $allowedTypes = ['jpg', 'jpeg', 'png', 'gif'], $maxSize = 5242880) { if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) { return false; } // Dateigröße prüfen if ($file['size'] > $maxSize) { return false; } // Dateityp prüfen $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimeType = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); $allowedMimes = [ 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif' ]; $validMimes = array_intersect_key($allowedMimes, array_flip($allowedTypes)); if (!in_array($mimeType, $validMimes)) { return false; } // Bildgröße prüfen $imageInfo = getimagesize($file['tmp_name']); if (!$imageInfo) { return false; } return true; } /** * Sichere Datei-Upload-Verarbeitung */ public function processSecureUpload($file, $destination, $allowedTypes = ['jpg', 'jpeg', 'png', 'gif']) { if (!$this->validateFileUpload($file, $allowedTypes)) { throw new \Exception('Invalid file upload'); } $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $filename = bin2hex(random_bytes(16)) . '.' . $extension; $filepath = $destination . '/' . $filename; if (!move_uploaded_file($file['tmp_name'], $filepath)) { throw new \Exception('Failed to move uploaded file'); } return $filename; } }