| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- <?php
- /**
- * Classe csrf
- *
- * Gère la protection contre les attaques CSRF (Cross-Site Request Forgery).
- * Cette classe permet de générer et valider des tokens CSRF pour sécuriser les formulaires.
- */
- class csrf
- {
- /**
- * Génère un token CSRF unique pour la session courante.
- * Si un token existe déjà, il est retourné sans en créer un nouveau.
- *
- * @param string $formName Nom du formulaire (permet d'avoir plusieurs tokens)
- * @return string Le token CSRF généré
- */
- public static function generateToken(string $formName = 'default'): string
- {
- // S'assurer qu'une session est active
- if (session_status() !== PHP_SESSION_ACTIVE) {
- secureSession::start();
- }
- // Initialiser le tableau de tokens si nécessaire
- if (!isset($_SESSION['csrf_tokens'])) {
- $_SESSION['csrf_tokens'] = [];
- }
- // Générer un nouveau token s'il n'existe pas pour ce formulaire
- if (!isset($_SESSION['csrf_tokens'][$formName])) {
- $_SESSION['csrf_tokens'][$formName] = [
- 'token' => bin2hex(random_bytes(32)),
- 'timestamp' => time()
- ];
- }
- return $_SESSION['csrf_tokens'][$formName]['token'];
- }
- /**
- * Valide un token CSRF.
- *
- * @param string $token Le token à valider
- * @param string $formName Nom du formulaire correspondant
- * @param int $maxAge Durée de validité du token en secondes (par défaut 3600 = 1h)
- * @return bool True si le token est valide, False sinon
- */
- public static function validateToken(string $token, string $formName = 'default', int $maxAge = 3600): bool
- {
- // Vérifier qu'une session est active
- if (session_status() !== PHP_SESSION_ACTIVE) {
- return false;
- }
- // Vérifier que le token existe pour ce formulaire
- if (!isset($_SESSION['csrf_tokens'][$formName])) {
- return false;
- }
- $storedData = $_SESSION['csrf_tokens'][$formName];
- // Vérifier l'expiration du token
- if ((time() - $storedData['timestamp']) > $maxAge) {
- self::removeToken($formName);
- return false;
- }
- // Comparer les tokens de manière sécurisée (timing-safe)
- $isValid = hash_equals($storedData['token'], $token);
- // Token à usage unique : supprimer après validation
- if ($isValid) {
- self::removeToken($formName);
- }
- return $isValid;
- }
- /**
- * Supprime un token CSRF spécifique.
- *
- * @param string $formName Nom du formulaire
- * @return bool True si le token a été supprimé
- */
- public static function removeToken(string $formName = 'default'): bool
- {
- if (isset($_SESSION['csrf_tokens'][$formName])) {
- unset($_SESSION['csrf_tokens'][$formName]);
- return true;
- }
- return false;
- }
- /**
- * Supprime tous les tokens CSRF expirés.
- * À appeler périodiquement pour nettoyer les tokens obsolètes.
- *
- * @param int $maxAge Âge maximum en secondes
- * @return int Nombre de tokens supprimés
- */
- public static function cleanExpiredTokens(int $maxAge = 3600): int
- {
- if (!isset($_SESSION['csrf_tokens']) || !is_array($_SESSION['csrf_tokens'])) {
- return 0;
- }
- $removed = 0;
- $currentTime = time();
- foreach ($_SESSION['csrf_tokens'] as $formName => $data) {
- if (($currentTime - $data['timestamp']) > $maxAge) {
- unset($_SESSION['csrf_tokens'][$formName]);
- $removed++;
- }
- }
- return $removed;
- }
- /**
- * Génère un champ input hidden contenant le token CSRF.
- * Pratique pour l'insertion directe dans les formulaires HTML.
- *
- * @param string $formName Nom du formulaire
- * @param string $fieldName Nom du champ input (par défaut 'csrf_token')
- * @return string Le code HTML du champ input
- */
- public static function inputField(string $formName = 'default', string $fieldName = 'csrf_token'): string
- {
- $token = self::generateToken($formName);
- return sprintf(
- '<input type="hidden" name="%s" value="%s" />',
- htmlspecialchars($fieldName, ENT_QUOTES, 'UTF-8'),
- htmlspecialchars($token, ENT_QUOTES, 'UTF-8')
- );
- }
- /**
- * Génère une balise meta pour les requêtes AJAX.
- * À placer dans le <head> de votre page HTML.
- *
- * @param string $formName Nom du formulaire
- * @return string Le code HTML de la balise meta
- */
- public static function metaTag(string $formName = 'default'): string
- {
- $token = self::generateToken($formName);
- return sprintf(
- '<meta name="csrf-token" content="%s" />',
- htmlspecialchars($token, ENT_QUOTES, 'UTF-8')
- );
- }
- /**
- * Vérifie un token CSRF depuis $_POST.
- *
- * @param string $formName Nom du formulaire
- * @param string $fieldName Nom du champ POST (par défaut 'csrf_token')
- * @param int $maxAge Durée de validité en secondes
- * @return bool True si le token POST est valide
- */
- public static function validatePost(string $formName = 'default', string $fieldName = 'csrf_token', int $maxAge = 3600): bool
- {
- if (!isset($_POST[$fieldName])) {
- return false;
- }
- return self::validateToken($_POST[$fieldName], $formName, $maxAge);
- }
- /**
- * Vérifie un token CSRF depuis un header HTTP (pour AJAX).
- *
- * @param string $formName Nom du formulaire
- * @param string $headerName Nom du header (par défaut 'X-CSRF-Token')
- * @param int $maxAge Durée de validité en secondes
- * @return bool True si le token du header est valide
- */
- public static function validateHeader(string $formName = 'default', string $headerName = 'X-CSRF-Token', int $maxAge = 3600): bool
- {
- $headers = getallheaders();
- if (!isset($headers[$headerName])) {
- return false;
- }
- return self::validateToken($headers[$headerName], $formName, $maxAge);
- }
- /**
- * Middleware de validation automatique.
- * Lance une exception si le token est invalide.
- *
- * @param string $formName Nom du formulaire
- * @param string $method Méthode de validation ('post' ou 'header')
- * @throws Exception Si le token est invalide
- */
- public static function protect(string $formName = 'default', string $method = 'post'): void
- {
- $isValid = false;
- switch (strtolower($method)) {
- case 'post':
- $isValid = self::validatePost($formName);
- break;
- case 'header':
- $isValid = self::validateHeader($formName);
- break;
- default:
- throw new Exception("Invalid CSRF validation method: $method");
- }
- if (!$isValid) {
- http_response_code(403);
- die(json_encode([
- 'success' => false,
- 'error' => 'CSRF token validation failed',
- 'message' => 'Requête non autorisée. Veuillez recharger la page.'
- ]));
- }
- }
- /**
- * Obtient des statistiques sur les tokens CSRF en session.
- *
- * @return array Statistiques (nombre de tokens, âge moyen, etc.)
- */
- public static function getStats(): array
- {
- if (!isset($_SESSION['csrf_tokens']) || !is_array($_SESSION['csrf_tokens'])) {
- return [
- 'count' => 0,
- 'forms' => []
- ];
- }
- $currentTime = time();
- $stats = [
- 'count' => count($_SESSION['csrf_tokens']),
- 'forms' => []
- ];
- foreach ($_SESSION['csrf_tokens'] as $formName => $data) {
- $stats['forms'][$formName] = [
- 'age' => $currentTime - $data['timestamp'],
- 'created_at' => date('Y-m-d H:i:s', $data['timestamp'])
- ];
- }
- return $stats;
- }
- }
|