| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- <?php
- /**
- * Classe loginSecurity
- *
- * Gère la sécurité avancée pour la page de login :
- * - Rate limiting (limitation du nombre de tentatives)
- * - Throttling (délai progressif après échecs)
- * - Logging des tentatives
- * - Verrouillage temporaire
- */
- class loginSecurity
- {
- // Configuration
- private const MAX_ATTEMPTS = 5; // Nombre max de tentatives avant blocage
- private const LOCKOUT_TIME = 900; // Durée du blocage en secondes (15 min)
- private const THROTTLE_BASE = 2; // Délai de base en secondes
- private const THROTTLE_MAX = 30; // Délai maximum en secondes
- private const LOG_TABLE = 'login_attempts'; // Table de log (si DB)
- /**
- * Vérifie si l'IP ou l'email est bloqué
- *
- * @param string $email Email tenté
- * @param string|null $ip Adresse IP (auto-détectée si null)
- * @return array ['blocked' => bool, 'remaining_time' => int, 'attempts' => int]
- */
- public static function checkBlocked(string $email = '', ?string $ip = null): array
- {
- $ip = $ip ?? self::getClientIP();
- $key = self::getCacheKey($ip, $email);
- // Récupérer les données de session
- $data = $_SESSION['login_security'][$key] ?? [
- 'attempts' => 0,
- 'last_attempt' => 0,
- 'locked_until' => 0
- ];
- // Vérifier si bloqué
- if ($data['locked_until'] > time()) {
- return [
- 'blocked' => true,
- 'remaining_time' => $data['locked_until'] - time(),
- 'attempts' => $data['attempts'],
- 'message' => self::getBlockedMessage($data['locked_until'] - time())
- ];
- }
- // Réinitialiser si le blocage est expiré
- if ($data['locked_until'] > 0 && $data['locked_until'] <= time()) {
- self::resetAttempts($ip, $email);
- $data['attempts'] = 0;
- }
- return [
- 'blocked' => false,
- 'remaining_time' => 0,
- 'attempts' => $data['attempts'],
- 'message' => ''
- ];
- }
- /**
- * Enregistre une tentative de connexion
- *
- * @param string $email Email tenté
- * @param bool $success Tentative réussie ou non
- * @param string|null $ip Adresse IP
- * @return array Statut après la tentative
- */
- public static function recordAttempt(string $email, bool $success, ?string $ip = null): array
- {
- $ip = $ip ?? self::getClientIP();
- $key = self::getCacheKey($ip, $email);
- // Log dans la base de données
- self::logAttempt($email, $ip, $success);
- // Si succès, réinitialiser les compteurs
- if ($success) {
- self::resetAttempts($ip, $email);
- return [
- 'blocked' => false,
- 'attempts' => 0,
- 'throttle_delay' => 0
- ];
- }
- // Incrémenter le compteur d'échecs
- $data = $_SESSION['login_security'][$key] ?? [
- 'attempts' => 0,
- 'last_attempt' => 0,
- 'locked_until' => 0
- ];
- $data['attempts']++;
- $data['last_attempt'] = time();
- // Vérifier si on doit bloquer
- if ($data['attempts'] >= self::MAX_ATTEMPTS) {
- $data['locked_until'] = time() + self::LOCKOUT_TIME;
- $_SESSION['login_security'][$key] = $data;
- // Log le blocage
- error_log(sprintf(
- "[SECURITY] IP %s blocked for %d seconds after %d failed attempts (email: %s)",
- $ip,
- self::LOCKOUT_TIME,
- $data['attempts'],
- $email
- ));
- return [
- 'blocked' => true,
- 'attempts' => $data['attempts'],
- 'throttle_delay' => 0,
- 'remaining_time' => self::LOCKOUT_TIME,
- 'message' => self::getBlockedMessage(self::LOCKOUT_TIME)
- ];
- }
- $_SESSION['login_security'][$key] = $data;
- // Calculer le délai de throttling
- $throttleDelay = self::calculateThrottleDelay($data['attempts']);
- return [
- 'blocked' => false,
- 'attempts' => $data['attempts'],
- 'remaining_attempts' => self::MAX_ATTEMPTS - $data['attempts'],
- 'throttle_delay' => $throttleDelay,
- 'message' => self::getWarningMessage($data['attempts'])
- ];
- }
- /**
- * Applique le délai de throttling (à appeler avant de traiter le login)
- *
- * @param string $email Email tenté
- * @param string|null $ip Adresse IP
- */
- public static function applyThrottle(string $email = '', ?string $ip = null): void
- {
- $ip = $ip ?? self::getClientIP();
- $key = self::getCacheKey($ip, $email);
- $data = $_SESSION['login_security'][$key] ?? ['attempts' => 0];
- if ($data['attempts'] > 0) {
- $delay = self::calculateThrottleDelay($data['attempts']);
- if ($delay > 0) {
- sleep($delay);
- }
- }
- }
- /**
- * Calcule le délai de throttling progressif
- *
- * @param int $attempts Nombre de tentatives
- * @return int Délai en secondes
- */
- private static function calculateThrottleDelay(int $attempts): int
- {
- if ($attempts <= 1) {
- return 0;
- }
- // Délai exponentiel : 2, 4, 8, 16, 30 (max)
- $delay = pow(self::THROTTLE_BASE, $attempts - 1);
- return min($delay, self::THROTTLE_MAX);
- }
- /**
- * Réinitialise les tentatives pour une IP/email
- *
- * @param string $ip Adresse IP
- * @param string $email Email
- */
- public static function resetAttempts(string $ip, string $email = ''): void
- {
- $key = self::getCacheKey($ip, $email);
- unset($_SESSION['login_security'][$key]);
- // Aussi réinitialiser par IP seule
- $keyIp = self::getCacheKey($ip, '');
- unset($_SESSION['login_security'][$keyIp]);
- }
- /**
- * Log une tentative dans la base de données
- *
- * @param string $email Email tenté
- * @param string $ip Adresse IP
- * @param bool $success Succès ou échec
- */
- private static function logAttempt(string $email, string $ip, bool $success): void
- {
- try {
- // Vérifier si la table existe
- $tableExists = db::bind("SHOW TABLES LIKE ?", [self::LOG_TABLE]);
- if (!empty($tableExists)) {
- db::bind(
- "INSERT INTO " . self::LOG_TABLE . " (email, ip_address, success, user_agent, attempted_at)
- VALUES (?, ?, ?, ?, NOW())",
- [
- $email,
- $ip,
- $success ? 1 : 0,
- $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'
- ]
- );
- }
- } catch (Exception $e) {
- // Si la table n'existe pas, log dans le fichier
- error_log(sprintf(
- "[LOGIN] %s | IP: %s | Email: %s | Status: %s | UA: %s",
- date('Y-m-d H:i:s'),
- $ip,
- $email,
- $success ? 'SUCCESS' : 'FAILED',
- $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'
- ));
- }
- }
- /**
- * Génère la clé de cache unique
- *
- * @param string $ip Adresse IP
- * @param string $email Email
- * @return string Clé de cache
- */
- private static function getCacheKey(string $ip, string $email): string
- {
- // Combiner IP + email hashé pour plus de sécurité
- $emailHash = $email ? md5(strtolower(trim($email))) : 'nomail';
- return "login_{$ip}_{$emailHash}";
- }
- /**
- * Récupère l'IP réelle du client
- *
- * @return string Adresse IP
- */
- public static function getClientIP(): string
- {
- $headers = [
- 'HTTP_CF_CONNECTING_IP', // Cloudflare
- 'HTTP_X_FORWARDED_FOR',
- 'HTTP_X_FORWARDED',
- 'HTTP_X_CLUSTER_CLIENT_IP',
- 'HTTP_FORWARDED_FOR',
- 'HTTP_FORWARDED',
- 'HTTP_CLIENT_IP',
- 'REMOTE_ADDR'
- ];
- foreach ($headers as $header) {
- if (!empty($_SERVER[$header])) {
- $ip = $_SERVER[$header];
- if (strpos($ip, ',') !== false) {
- $ips = explode(',', $ip);
- $ip = trim($ips[0]);
- }
- if (filter_var($ip, FILTER_VALIDATE_IP)) {
- return $ip;
- }
- }
- }
- return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
- }
- /**
- * Message de blocage formaté
- *
- * @param int $seconds Secondes restantes
- * @return string Message
- */
- private static function getBlockedMessage(int $seconds): string
- {
- $minutes = ceil($seconds / 60);
- if ($minutes <= 1) {
- return "Trop de tentatives. Réessayez dans $seconds secondes.";
- }
- return "Trop de tentatives. Réessayez dans $minutes minutes.";
- }
- /**
- * Message d'avertissement
- *
- * @param int $attempts Nombre de tentatives
- * @return string Message
- */
- private static function getWarningMessage(int $attempts): string
- {
- $remaining = self::MAX_ATTEMPTS - $attempts;
- if ($remaining <= 2) {
- return "Attention : $remaining tentative(s) restante(s) avant blocage.";
- }
- return '';
- }
- /**
- * Vérifie un honeypot (champ caché anti-bot)
- *
- * @param string $fieldName Nom du champ honeypot
- * @return bool True si le honeypot est vide (utilisateur légitime)
- */
- public static function checkHoneypot(string $fieldName = 'website'): bool
- {
- // Le champ honeypot doit être VIDE
- // Les bots le remplissent souvent automatiquement
- if (isset($_POST[$fieldName]) && !empty($_POST[$fieldName])) {
- error_log(sprintf(
- "[SECURITY] Honeypot triggered | IP: %s | Field: %s | Value: %s",
- self::getClientIP(),
- $fieldName,
- substr($_POST[$fieldName], 0, 50)
- ));
- return false;
- }
- return true;
- }
- /**
- * Obtient les statistiques de tentatives pour une IP
- *
- * @param string|null $ip Adresse IP
- * @return array Statistiques
- */
- public static function getStats(?string $ip = null): array
- {
- $ip = $ip ?? self::getClientIP();
- $stats = [
- 'ip' => $ip,
- 'total_attempts' => 0,
- 'blocked_keys' => []
- ];
- if (isset($_SESSION['login_security'])) {
- foreach ($_SESSION['login_security'] as $key => $data) {
- if (strpos($key, $ip) !== false) {
- $stats['total_attempts'] += $data['attempts'] ?? 0;
- if (isset($data['locked_until']) && $data['locked_until'] > time()) {
- $stats['blocked_keys'][] = $key;
- }
- }
- }
- }
- return $stats;
- }
- /**
- * Nettoie les anciennes entrées de session
- */
- public static function cleanup(): void
- {
- if (!isset($_SESSION['login_security'])) {
- return;
- }
- $now = time();
- foreach ($_SESSION['login_security'] as $key => $data) {
- // Supprimer les entrées expirées depuis plus d'1 heure
- if (isset($data['locked_until']) && $data['locked_until'] > 0) {
- if ($data['locked_until'] + 3600 < $now) {
- unset($_SESSION['login_security'][$key]);
- }
- } elseif (isset($data['last_attempt']) && $data['last_attempt'] + 3600 < $now) {
- unset($_SESSION['login_security'][$key]);
- }
- }
- }
- }
|