2024-12-25 13:05:50 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
class Security {
|
|
|
|
private static $instance = null;
|
|
|
|
private $rate_limit_file;
|
|
|
|
private static $encryptionKey = 'your-secure-encryption-key-here';
|
|
|
|
|
|
|
|
private function __construct() {
|
|
|
|
$this->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);
|
|
|
|
}
|
2024-12-25 14:31:31 +02:00
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
2024-12-25 13:05:50 +02:00
|
|
|
}
|