Newwebshop/app/Core/Security.php

500 lines
15 KiB
PHP

<?php
/**
* Copyright seit 2024 Webshop System
*
* Security Core-Klasse für das Webshop-System
*
* @author Webshop System
* @license GPL v3
*/
namespace App\Core;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
class Security
{
private $conn;
private $session;
private $config;
// Rate Limiting Konfiguration
private $rateLimits = [
'login' => ['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;
}
}