500 lines
15 KiB
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;
|
|
}
|
|
}
|