rate_limit_file = __DIR__ . '/../cache/rate_limits.json'; if (!file_exists($this->rate_limit_file)) { if (!is_dir(dirname($this->rate_limit_file))) { mkdir(dirname($this->rate_limit_file), 0755, true); } file_put_contents($this->rate_limit_file, '{}'); } } public static function getInstance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } public static function sanitizeInput($data) { if (is_array($data)) { return array_map([self::class, 'sanitizeInput'], $data); } return htmlspecialchars(strip_tags(trim($data)), ENT_QUOTES, 'UTF-8'); } public static function validateRecaptcha($response) { $url = 'https://www.google.com/recaptcha/api/siteverify'; $data = [ 'secret' => RECAPTCHA_SECRET_KEY, 'response' => $response ]; $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); $result_json = json_decode($result, true); return isset($result_json['success']) && $result_json['success'] === true; } public function checkRateLimit($ip, $endpoint, $limit = 60, $window = 3600) { $limits = json_decode(file_get_contents($this->rate_limit_file), true); $now = time(); $key = $ip . ':' . $endpoint; if (!isset($limits[$key])) { $limits[$key] = ['count' => 0, 'window_start' => $now]; } if ($now - $limits[$key]['window_start'] > $window) { $limits[$key] = ['count' => 0, 'window_start' => $now]; } $limits[$key]['count']++; file_put_contents($this->rate_limit_file, json_encode($limits)); return $limits[$key]['count'] <= $limit; } public static function generateCSRFToken() { if (!isset($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } public static function verifyCSRFToken($token) { return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); } public static function encrypt($data) { $iv = random_bytes(16); $encrypted = openssl_encrypt( $data, 'AES-256-CBC', self::$encryptionKey, 0, $iv ); return base64_encode($iv . $encrypted); } public static function decrypt($data) { $data = base64_decode($data); $iv = substr($data, 0, 16); $encrypted = substr($data, 16); return openssl_decrypt( $encrypted, 'AES-256-CBC', self::$encryptionKey, 0, $iv ); } public static function hashPassword($password) { return password_hash($password, PASSWORD_DEFAULT); } public static function verifyPassword($password, $hash) { return password_verify($password, $hash); } public static function sanitize_input($data) { $data = trim($data); $data = stripslashes($data); $data = htmlspecialchars($data, ENT_QUOTES, 'UTF-8'); return $data; } public static function generate_csrf_token() { if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } public static function verify_csrf_token($token) { return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); } public static function check_rate_limit($key, $limit = 5, $period = 300) { $cache_key = "rate_limit:{$key}"; $current = isset($_SESSION[$cache_key]) ? $_SESSION[$cache_key] : ['count' => 0, 'timestamp' => time()]; if (time() - $current['timestamp'] > $period) { $current = ['count' => 1, 'timestamp' => time()]; } else { $current['count']++; } $_SESSION[$cache_key] = $current; return $current['count'] <= $limit; } public static function is_password_strong($password) { // Minimum 8 characters if (strlen($password) < 8) return false; // Must contain at least one uppercase letter if (!preg_match('/[A-Z]/', $password)) return false; // Must contain at least one lowercase letter if (!preg_match('/[a-z]/', $password)) return false; // Must contain at least one number if (!preg_match('/[0-9]/', $password)) return false; // Must contain at least one special character if (!preg_match('/[!@#$%^&*()\-_=+{};:,<.>]/', $password)) return false; return true; } public static function is_file_upload_safe($file) { $allowed_types = ['image/jpeg', 'image/png', 'image/webp']; $max_size = 5 * 1024 * 1024; // 5MB // Check file type if (!in_array($file['type'], $allowed_types)) { return false; } // Check file size if ($file['size'] > $max_size) { return false; } // Check if file is actually an image if (!getimagesize($file['tmp_name'])) { return false; } return true; } public static function set_secure_headers() { header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: SAMEORIGIN'); header('X-XSS-Protection: 1; mode=block'); header('Referrer-Policy: strict-origin-when-cross-origin'); header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'"); } public static function secure_session_start() { ini_set('session.cookie_httponly', 1); ini_set('session.cookie_secure', 1); ini_set('session.use_only_cookies', 1); ini_set('session.cookie_samesite', 'Strict'); session_start(); } public static function log_security_event($event_type, $details) { $log_file = __DIR__ . '/../logs/security.log'; $timestamp = date('Y-m-d H:i:s'); $ip = $_SERVER['REMOTE_ADDR']; $user = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : 'anonymous'; $log_entry = sprintf( "[%s] %s - IP: %s, User: %s, Details: %s\n", $timestamp, $event_type, $ip, $user, json_encode($details) ); error_log($log_entry, 3, $log_file); } public static function init_security() { self::secure_session_start(); self::set_secure_headers(); // Force HTTPS if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') { header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']); exit(); } } }