Pārlūkot izejas kodu

feat(security): Implement secure session management and CSRF protection

- Added secureSession class to manage secure session initiation, regeneration, and destruction.
- Integrated secureSession into existing session management in session.class.php.
- Updated various controllers and API endpoints to utilize secureSession for session handling.
- Implemented session timeout checks and secure logout functionality.
- Added a test script for validating session security features and CSRF token generation/validation.
- Enhanced .htaccess for enforcing HTTPS connections.
stany.ferer 2 nedēļas atpakaļ
vecāks
revīzija
0bac3c0f3b

+ 6 - 0
.gitignore

@@ -12,3 +12,9 @@ env.inc.php
 .codegpt
 blacklist/ip_attempts.log
 blacklist/ip.txt
+blacklist/ip_attempts.log
+public-cms/.htaccess
+blacklist/ip_attempts.log
+blacklist/ip_attempts.log
+blacklist/ip_attempts.log
+.specstory/history/*

+ 4 - 0
.specstory/.gitignore

@@ -0,0 +1,4 @@
+# SpecStory explanation file
+/.what-is-this.md
+# SpecStory project identity file
+/.project.json

+ 253 - 0
core/class/csrf.class.php

@@ -0,0 +1,253 @@
+<?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;
+    }
+}

+ 267 - 0
core/class/secureSession.class.php

@@ -0,0 +1,267 @@
+<?php
+
+/**
+ * Classe secureSession
+ * 
+ * Gère le démarrage sécurisé des sessions avec les paramètres de sécurité recommandés.
+ * Cette classe centralise la configuration de sécurité des sessions PHP.
+ */
+class secureSession
+{
+    /**
+     * Démarre une session sécurisée avec les paramètres de sécurité recommandés.
+     * 
+     * @param array $options Options supplémentaires pour la configuration de session
+     * @return bool True si la session a été démarrée avec succès
+     */
+    public static function start(array $options = []): bool
+    {
+        // Si une session est déjà active, ne pas en redémarrer une nouvelle
+        if (session_status() === PHP_SESSION_ACTIVE) {
+            return true;
+        }
+
+        // Configuration de sécurité des cookies de session
+        $cookieParams = [
+            'lifetime' => $options['lifetime'] ?? 0, // Session cookie (expire à la fermeture du navigateur)
+            'path' => $options['path'] ?? '/',
+            'domain' => $options['domain'] ?? '',
+            'secure' => $options['secure'] ?? self::isHttps(), // HTTPS uniquement si disponible
+            'httponly' => true, // Empêche l'accès JavaScript au cookie de session
+            'samesite' => $options['samesite'] ?? 'Lax' // Protection CSRF (Lax ou Strict)
+        ];
+
+        // Applique les paramètres des cookies de session
+        session_set_cookie_params($cookieParams);
+
+        // Configuration supplémentaire de sécurité
+        ini_set('session.use_strict_mode', '1'); // Refuse les IDs de session non initialisés
+        ini_set('session.use_only_cookies', '1'); // Utilise uniquement les cookies, pas l'URL
+        ini_set('session.cookie_httponly', '1'); // Protection XSS
+        ini_set('session.cookie_samesite', $cookieParams['samesite']); // Protection CSRF
+
+        // Si HTTPS est disponible, force les cookies sécurisés
+        if ($cookieParams['secure']) {
+            ini_set('session.cookie_secure', '1');
+        }
+
+        // Utilise un algorithme de hachage fort pour les IDs de session
+        ini_set('session.hash_function', 'sha256');
+        ini_set('session.hash_bits_per_character', '5');
+
+        // Régénération de l'ID de session pour prévenir la fixation de session
+        if (isset($options['session_name'])) {
+            session_name($options['session_name']);
+        }
+
+        // Démarre la session
+        $started = session_start();
+
+        // Régénère l'ID de session périodiquement pour plus de sécurité
+        if ($started && !isset($_SESSION['_session_created'])) {
+            $_SESSION['_session_created'] = time();
+            $_SESSION['_session_ip'] = self::getClientIP();
+            $_SESSION['_session_user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
+        }
+
+        // Régénère l'ID toutes les 30 minutes
+        if ($started && isset($_SESSION['_session_created'])) {
+            $sessionAge = time() - $_SESSION['_session_created'];
+
+            if ($sessionAge > 1800) { // 30 minutes
+                self::regenerateId();
+                $_SESSION['_session_created'] = time();
+            }
+        }
+
+        // Validation de l'intégrité de la session
+        if ($started && !self::validateSession()) {
+            self::destroy();
+            return self::start($options); // Redémarre une nouvelle session propre
+        }
+
+        return $started;
+    }
+
+    /**
+     * Régénère l'ID de session de manière sécurisée.
+     * 
+     * @return bool True si la régénération a réussi
+     */
+    public static function regenerateId(): bool
+    {
+        if (session_status() !== PHP_SESSION_ACTIVE) {
+            return false;
+        }
+
+        return session_regenerate_id(true); // true = supprime l'ancienne session
+    }
+
+    /**
+     * Détruit complètement une session de manière sécurisée.
+     * 
+     * @return bool True si la session a été détruite avec succès
+     */
+    public static function destroy(): bool
+    {
+        if (session_status() !== PHP_SESSION_ACTIVE) {
+            return true;
+        }
+
+        // Efface toutes les variables de session
+        $_SESSION = [];
+
+        // Détruit le cookie de session
+        if (ini_get('session.use_cookies')) {
+            $params = session_get_cookie_params();
+            setcookie(
+                session_name(),
+                '',
+                time() - 42000,
+                $params['path'],
+                $params['domain'],
+                $params['secure'],
+                $params['httponly']
+            );
+        }
+
+        // Détruit la session
+        return session_destroy();
+    }
+
+    /**
+     * Valide l'intégrité de la session en vérifiant l'IP et le User-Agent.
+     * 
+     * @return bool True si la session est valide
+     */
+    private static function validateSession(): bool
+    {
+        // Vérification de l'IP (optionnelle, peut causer des problèmes avec certains FAI)
+        if (isset($_SESSION['_session_ip'])) {
+            $currentIP = self::getClientIP();
+            if ($_SESSION['_session_ip'] !== $currentIP) {
+                // Option stricte : retourner false
+                // Pour l'instant, on log seulement
+                error_log("Session IP mismatch: {$_SESSION['_session_ip']} vs $currentIP");
+            }
+        }
+
+        // Vérification du User-Agent
+        if (isset($_SESSION['_session_user_agent'])) {
+            $currentUA = $_SERVER['HTTP_USER_AGENT'] ?? '';
+            if ($_SESSION['_session_user_agent'] !== $currentUA) {
+                error_log("Session User-Agent mismatch");
+                return false; // Plus strict pour le User-Agent
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Détecte si la connexion est en HTTPS.
+     * 
+     * @return bool True si HTTPS est activé
+     */
+    private static function isHttps(): bool
+    {
+        if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
+            return true;
+        }
+
+        if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
+            return true;
+        }
+
+        if (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') {
+            return true;
+        }
+
+        return (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443);
+    }
+
+    /**
+     * Récupère l'IP réelle du client (gère les proxies).
+     * 
+     * @return string L'adresse IP du client
+     */
+    private 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];
+                // Si plusieurs IPs (proxy chain), prendre la première
+                if (strpos($ip, ',') !== false) {
+                    $ips = explode(',', $ip);
+                    $ip = trim($ips[0]);
+                }
+                // Valider que c'est une IP valide
+                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
+                    return $ip;
+                }
+            }
+        }
+
+        return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
+    }
+
+    /**
+     * Configure le timeout d'inactivité de session.
+     * 
+     * @param int $minutes Nombre de minutes d'inactivité avant expiration
+     * @return bool True si le timeout est configuré
+     */
+    public static function setInactivityTimeout(int $minutes): bool
+    {
+        if (session_status() !== PHP_SESSION_ACTIVE) {
+            return false;
+        }
+
+        $timeout = $minutes * 60;
+
+        if (isset($_SESSION['_session_last_activity'])) {
+            $elapsed = time() - $_SESSION['_session_last_activity'];
+
+            if ($elapsed > $timeout) {
+                self::destroy();
+                return false;
+            }
+        }
+
+        $_SESSION['_session_last_activity'] = time();
+        return true;
+    }
+
+    /**
+     * Récupère les informations de sécurité de la session actuelle.
+     * 
+     * @return array Informations de sécurité
+     */
+    public static function getSecurityInfo(): array
+    {
+        if (session_status() !== PHP_SESSION_ACTIVE) {
+            return [];
+        }
+
+        return [
+            'session_id' => session_id(),
+            'session_name' => session_name(),
+            'created' => $_SESSION['_session_created'] ?? null,
+            'last_activity' => $_SESSION['_session_last_activity'] ?? null,
+            'ip' => $_SESSION['_session_ip'] ?? null,
+            'is_https' => self::isHttps(),
+            'cookie_params' => session_get_cookie_params()
+        ];
+    }
+}

+ 60 - 10
core/class/session.class.php

@@ -1,4 +1,5 @@
 <?php
+
 /**
  * Classe session
  * 
@@ -92,7 +93,8 @@ class session
      * @param array $_type Types d'accès autorisés.
      * @return bool Vrai si l'utilisateur a accès, faux sinon.
      */
-    public static function access(array $_type){
+    public static function access(array $_type)
+    {
         return (in_array($_SESSION["user"]["idType"], $_type)) ? TRUE : FALSE;
     }
 
@@ -101,7 +103,8 @@ class session
      *
      * @return bool Vrai si l'utilisateur est dans l'espace des contrôleurs, faux sinon.
      */
-    public static function isEspaceControleurs(){
+    public static function isEspaceControleurs()
+    {
         return ($_SERVER['HTTP_HOST'] == DOMAIN_CONTROL) ? TRUE : FALSE;
     }
 
@@ -110,7 +113,8 @@ class session
      *
      * @return bool Vrai si l'utilisateur est dans l'espace des salariés, faux sinon.
      */
-    public static function isEspaceSalaries(){
+    public static function isEspaceSalaries()
+    {
         return ($_SERVER['HTTP_HOST'] == DOMAIN_EVENTS) ? TRUE : FALSE;
     }
 
@@ -120,8 +124,9 @@ class session
      * @param array $_array Données temporaires à définir.
      * @param string|null $_name Nom de la clé temporaire (par défaut "tmp").
      */
-    public static function setTemp(array $_array, ?string $_name = NULL){
-        if($_name == NULL){
+    public static function setTemp(array $_array, ?string $_name = NULL)
+    {
+        if ($_name == NULL) {
             $_SESSION["TEMP"]["tmp"] = $_array;
         } else {
             $_SESSION["TEMP"][$_name] = $_array;
@@ -134,10 +139,11 @@ class session
      * @param string|null $_name Nom de la clé temporaire (par défaut "tmp").
      * @return mixed Données temporaires récupérées.
      */
-    public static function getTemp(?string $_name = NULL){
-        if(empty($_SESSION["TEMP"])){ 
-            return NULL; 
-        } elseif($_name == NULL){
+    public static function getTemp(?string $_name = NULL)
+    {
+        if (empty($_SESSION["TEMP"])) {
+            return NULL;
+        } elseif ($_name == NULL) {
             $return = $_SESSION["TEMP"]["tmp"];
         } else {
             $return = $_SESSION["TEMP"][$_name];
@@ -151,8 +157,52 @@ class session
      *
      * @param string|null $_name Nom de la clé temporaire à réinitialiser (par défaut toutes).
      */
-    public static function resetTemp(?string $_name = NULL){
+    public static function resetTemp(?string $_name = NULL)
+    {
         unset($_SESSION["TEMP"]);
     }
 
+    /**
+     * Déconnexion sécurisée de l'utilisateur.
+     * Détruit la session et régénère l'ID pour éviter la fixation de session.
+     *
+     * @param string $_type Type de session à détruire (par défaut "user").
+     * @return bool True si la déconnexion a réussi.
+     */
+    public static function logout(string $_type = "user"): bool
+    {
+        // Supprimer les données de session de l'utilisateur
+        if (isset($_SESSION[$_type])) {
+            unset($_SESSION[$_type]);
+        }
+
+        // Si aucune autre donnée de session n'existe, détruire complètement la session
+        $hasOtherSessions = false;
+        foreach ($_SESSION as $key => $value) {
+            if (!in_array($key, ['_session_created', '_session_ip', '_session_user_agent', '_session_last_activity', 'TEMP'])) {
+                $hasOtherSessions = true;
+                break;
+            }
+        }
+
+        // Si pas d'autres sessions actives, détruire complètement
+        if (!$hasOtherSessions) {
+            return secureSession::destroy();
+        }
+
+        // Sinon, régénérer l'ID de session pour sécurité
+        return secureSession::regenerateId();
+    }
+
+    /**
+     * Force le timeout d'inactivité de la session.
+     * À appeler au début de chaque requête pour vérifier l'activité.
+     *
+     * @param int $minutes Nombre de minutes d'inactivité avant expiration (par défaut 30).
+     * @return bool True si la session est toujours valide, False si elle a expiré.
+     */
+    public static function checkTimeout(int $minutes = 30): bool
+    {
+        return secureSession::setInactivityTimeout($minutes);
+    }
 }

+ 12 - 10
core/controllers/header.php

@@ -3,40 +3,42 @@
 setlocale(LC_TIME, 'fr_FR');
 date_default_timezone_set(TIME_ZONE);
 
-require DIR_PHP_LIBS.'PHPMailer/Exception.php';
-require DIR_PHP_LIBS.'PHPMailer/PHPMailer.php';
-require DIR_PHP_LIBS.'PHPMailer/SMTP.php';
+require DIR_PHP_LIBS . 'PHPMailer/Exception.php';
+require DIR_PHP_LIBS . 'PHPMailer/PHPMailer.php';
+require DIR_PHP_LIBS . 'PHPMailer/SMTP.php';
 
 use PHPMailer\PHPMailer\PHPMailer;
 use PHPMailer\PHPMailer\Exception;
 
 spl_autoload_register(function ($class_name) {
-    (file_exists(DIR_PHP_CLASS.'/'.$class_name.'.class.php'))?	
-    require_once DIR_PHP_CLASS.'/'.$class_name.'.class.php' : '';
+    (file_exists(DIR_PHP_CLASS . '/' . $class_name . '.class.php')) ?
+        require_once DIR_PHP_CLASS . '/' . $class_name . '.class.php' : '';
 });
 
 // Filtre les IP authorisés à accéder au site
-if(!is_null(WHITE_IP)){
-    if(!in_array(htmlspecialchars(core::getUserIP()), WHITE_IP)){
+if (!is_null(WHITE_IP)) {
+    $userIP = core::getUserIP();
+    // Validation de l'IP
+    if (filter_var($userIP, FILTER_VALIDATE_IP) === false || !in_array($userIP, WHITE_IP)) {
         header("Location: /noAccess.php");
         exit();
     }
 }
 
 // Vérifier si IP balcklistée
-if(blacklist::isValidIPv4()){
+if (blacklist::isValidIPv4()) {
     blacklist::itIs();
 }
 
 // Si le site est en mode debug
-if(debug::isFile("debug")){
+if (debug::isFile("debug")) {
     error_reporting(E_ALL);
     ini_set("display_errors", 1);
     debug::startTimer();
 }
 
 // Si le site est en maintenance
-if(debug::isFile("maintenance") AND $_SERVER['HTTP_HOST'] != DOMAIN_CMS){
+if (debug::isFile("maintenance") and $_SERVER['HTTP_HOST'] != DOMAIN_CMS) {
     get::page("maintenance");
     exit();
 }

+ 7 - 7
public-cms/api/authenticator/index.php

@@ -1,19 +1,19 @@
 <?php
 
-session_start(); 
-
-require_once "../../../env.inc.php"; 
-require_once DOCUMENT_ROOT."/access.inc.php";
-require_once DOCUMENT_ROOT."/conf.inc.php";
+require_once "../../../env.inc.php";
+require_once DOCUMENT_ROOT . "/conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
+
+secureSession::start();
+require_once DOCUMENT_ROOT . "/access.inc.php";
 require_once "path_to_jwt.php"; // Inclure la classe gérant les JWT
 
-if(core::ifPost("email") AND core::ifPost("password")){
+if (core::ifPost("email") and core::ifPost("password")) {
     try {
         // Appel de l'authentification via JWT
         $authResponse = jwt::authenticate(core::getPost());
 
-        if(isset($authResponse["token"])) {
+        if (isset($authResponse["token"])) {
             // L'authentification est réussie et le JWT est généré
             historique::recRef("/api/authenticator/");
             historique::add(array(

+ 6 - 6
public-cms/api/checkSession/index.php

@@ -1,12 +1,12 @@
 <?php
 
-session_start(); 
-
-require_once "../../../env.inc.php"; 
-require_once DOCUMENT_ROOT."/access.inc.php";
-require_once DOCUMENT_ROOT."/conf.inc.php";
+require_once "../../../env.inc.php";
+require_once DOCUMENT_ROOT . "/conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
 
+secureSession::start();
+require_once DOCUMENT_ROOT . "/access.inc.php";
+
 if ($_SERVER['REQUEST_METHOD'] === 'GET') {
     echo jwt::checkSession();
-}
+}

+ 8 - 8
public-cms/api/checkToken/index.php

@@ -1,20 +1,20 @@
 <?php
 
-session_start(); 
-
-require_once "../../../env.inc.php"; 
-require_once DOCUMENT_ROOT."/access.inc.php";
-require_once DOCUMENT_ROOT."/conf.inc.php";
+require_once "../../../env.inc.php";
+require_once DOCUMENT_ROOT . "/conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
 
+secureSession::start();
+require_once DOCUMENT_ROOT . "/access.inc.php";
+
 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
 
     $json = file_get_contents('php://input');
     $tmp = json_decode($json, true);
 
     // Vérification de ce qui est décodé
-    if(isset($tmp["token"])) {
-        if(jwt::validateToken($tmp["token"])){
+    if (isset($tmp["token"])) {
+        if (jwt::validateToken($tmp["token"])) {
             echo json_encode(["status" => "success"]);
         } else {
             echo json_encode(["status" => "error", "message" => "Invalide Token"]);
@@ -22,4 +22,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
     } else {
         echo json_encode(["status" => "error", "message" => "Token not provided"]);
     }
-}
+}

+ 8 - 8
public-cms/api/refreshToken/index.php

@@ -1,19 +1,19 @@
 <?php
 
-session_start(); 
+require_once "../../../env.inc.php";
+require_once DOCUMENT_ROOT . "/conf.inc.php";
+require_once DIR_PHP_LAYOUTS . "header.php";
 
-require_once "../../../env.inc.php"; 
-require_once DOCUMENT_ROOT."/access.inc.php";
-require_once DOCUMENT_ROOT."/conf.inc.php";
-require_once DIR_PHP_LAYOUTS . "header.php"; 
+secureSession::start();
+require_once DOCUMENT_ROOT . "/access.inc.php";
 
 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
 
     $json = file_get_contents('php://input');
     $tmp = json_decode($json, true);
 
-    if(isset($tmp["token"])) {
-        if(jwt::validateToken($tmp["token"])){
+    if (isset($tmp["token"])) {
+        if (jwt::validateToken($tmp["token"])) {
             // Refresh Token 
             $json = jwt::refreshToken($tmp["token"]);
             $newToken = json_decode($json, true)["token"];
@@ -24,4 +24,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
     } else {
         echo json_encode(["status" => "error", "message" => "Token not provided"]);
     }
-}
+}

+ 3 - 3
public-cms/console.logs.php

@@ -1,11 +1,11 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
-require_once "../access.inc.php";
 require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
+
+secureSession::start();
+require_once "../access.inc.php";
 require_once DIR_PHP_LAYOUTS . "cms.session.php";
 
 switch (core::getGet("l")) {

+ 5 - 6
public-cms/cron.php

@@ -1,16 +1,15 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
-require_once "../access.inc.php";
 require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
 
+secureSession::start();
+require_once "../access.inc.php";
+
 if (cron::ifLocalHost()) {
-        // Email d'assignation
-        cron::sendMailAssignDocument();
-        
+    // Email d'assignation
+    cron::sendMailAssignDocument();
 } else {
     header('HTTP/1.0 401 Unauthorized');
     header('Content-Type: text/html; charset=utf-8');

+ 4 - 4
public-cms/document.php

@@ -1,11 +1,11 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
-require_once "../access.inc.php";
 require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
+
+secureSession::start();
+require_once "../access.inc.php";
 require_once DIR_PHP_LAYOUTS . "cms.session.php";
 
-core::ifGet("download") ? document::getFile(core::getGet("id")) : document::printFile(core::getGet("id"));
+core::ifGet("download") ? document::getFile(core::getGet("id")) : document::printFile(core::getGet("id"));

+ 5 - 4
public-cms/index.php

@@ -1,9 +1,10 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
-require_once "../access.inc.php";
 require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
-require_once DIR_PHP_LAYOUTS . "cms.index.php";
+
+secureSession::start();
+
+require_once "../access.inc.php";
+require_once DIR_PHP_LAYOUTS . "cms.index.php";

+ 5 - 4
public-cms/json.php

@@ -1,14 +1,15 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
-require_once "../access.inc.php";
 require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
+
+secureSession::start();
+
+require_once "../access.inc.php";
 require_once DIR_PHP_LAYOUTS . "cms.session.php";
 
 header('Content-Type: application/json');
 
 get::json();
-get::jsonData();
+get::jsonData();

+ 6 - 6
public-cms/json.refresh.php

@@ -1,19 +1,19 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
-require_once "../access.inc.php";
 require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
+
+secureSession::start();
+require_once "../access.inc.php";
 require_once DIR_PHP_LAYOUTS . "events.session.php";
 
-if(core::ifGet("data")){
-    if(json::create(core::getGet("data"))){
+if (core::ifGet("data")) {
+    if (json::create(core::getGet("data"))) {
         header('Content-Type: application/json');
         echo json_encode(['status' => 'ok']);
     } else {
-        http_response_code(500); 
+        http_response_code(500);
         echo json_encode(['status' => 'error']);
     }
 } else {

+ 4 - 4
public-cms/print.php

@@ -1,11 +1,11 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
-require_once "../access.inc.php";
 require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
+
+secureSession::start();
+require_once "../access.inc.php";
 require_once DIR_PHP_LAYOUTS . "cms.session.php";
 
-printPaper::switch(core::getGet("p"), core::getGet("id"));
+printPaper::switch(core::getGet("p"), core::getGet("id"));

+ 8 - 8
public-cms/qrcode.php

@@ -1,18 +1,18 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
-require_once "../access.inc.php";
-require_once "../conf.inc.php"; 
+require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
 
-header ('Content-Type: image/png');
+secureSession::start();
+require_once "../access.inc.php";
+
+header('Content-Type: image/png');
 
-if(core::ifGet("q")){
+if (core::ifGet("q")) {
     $link = core::base64_url_decode(core::getGet("q"));
 } else {
-    $link = "https://".DOMAIN_EVENTS;
+    $link = "https://" . DOMAIN_EVENTS;
 }
 
-myQrcode::show($link);
+myQrcode::show($link);

+ 5 - 5
public-cms/submit.php

@@ -1,14 +1,14 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
+require_once "../conf.inc.php";
+require_once DIR_PHP_LAYOUTS . "header.php";
+
+secureSession::start();
 require_once "../access.inc.php";
-require_once "../conf.inc.php"; 
-require_once DIR_PHP_LAYOUTS . "header.php"; 
 require_once DIR_PHP_LAYOUTS . "cms.session.php";
 
-if(debug::isFile("submit") AND (core::ifPost() OR core::ifFiles())) {
+if (debug::isFile("submit") and (core::ifPost() or core::ifFiles())) {
         core::ifPost() ? debug::logSession(core::getPost()) : NULL;
         core::ifFiles() ? debug::logSession(core::getFiles()) : NULL;
 }

+ 153 - 0
public-cms/test-security.php

@@ -0,0 +1,153 @@
+<?php
+
+/**
+ * Script de test pour la sécurisation des sessions et CSRF
+ * 
+ * Ce script permet de vérifier que toutes les fonctionnalités de sécurité
+ * sont correctement implémentées.
+ * 
+ * ⚠️ À SUPPRIMER EN PRODUCTION
+ */
+
+require_once "../env.inc.php";
+
+// Test 1 : Démarrage de session sécurisée
+echo "=== TEST 1 : Démarrage session sécurisée ===\n";
+try {
+    $started = secureSession::start();
+    echo $started ? "✅ Session démarrée avec succès\n" : "❌ Échec démarrage session\n";
+} catch (Exception $e) {
+    echo "❌ Erreur : " . $e->getMessage() . "\n";
+}
+
+// Test 2 : Informations de sécurité de la session
+echo "\n=== TEST 2 : Informations de sécurité ===\n";
+try {
+    $info = secureSession::getSecurityInfo();
+    echo "✅ Session ID : " . substr($info['session_id'], 0, 10) . "...\n";
+    echo "✅ Session Name : " . $info['session_name'] . "\n";
+    echo "✅ HTTPS : " . ($info['is_https'] ? 'Oui' : 'Non') . "\n";
+    echo "✅ HttpOnly : " . ($info['cookie_params']['httponly'] ? 'Oui' : 'Non') . "\n";
+    echo "✅ Secure : " . ($info['cookie_params']['secure'] ? 'Oui' : 'Non') . "\n";
+    echo "✅ SameSite : " . $info['cookie_params']['samesite'] . "\n";
+} catch (Exception $e) {
+    echo "❌ Erreur : " . $e->getMessage() . "\n";
+}
+
+// Test 3 : Génération de tokens CSRF
+echo "\n=== TEST 3 : Génération tokens CSRF ===\n";
+try {
+    $token1 = csrf::generateToken('test-form-1');
+    $token2 = csrf::generateToken('test-form-2');
+    echo "✅ Token 1 généré : " . substr($token1, 0, 16) . "...\n";
+    echo "✅ Token 2 généré : " . substr($token2, 0, 16) . "...\n";
+    echo ($token1 !== $token2) ? "✅ Les tokens sont différents\n" : "❌ Les tokens sont identiques (problème)\n";
+} catch (Exception $e) {
+    echo "❌ Erreur : " . $e->getMessage() . "\n";
+}
+
+// Test 4 : Validation de token CSRF
+echo "\n=== TEST 4 : Validation token CSRF ===\n";
+try {
+    $testToken = csrf::generateToken('validation-test');
+    $isValid = csrf::validateToken($testToken, 'validation-test');
+    echo $isValid ? "✅ Validation token réussie\n" : "❌ Validation token échouée\n";
+
+    // Le token devrait être supprimé après validation
+    $isValidAgain = csrf::validateToken($testToken, 'validation-test');
+    echo !$isValidAgain ? "✅ Token correctement supprimé après validation\n" : "❌ Token pas supprimé (problème)\n";
+} catch (Exception $e) {
+    echo "❌ Erreur : " . $e->getMessage() . "\n";
+}
+
+// Test 5 : Génération HTML pour formulaires
+echo "\n=== TEST 5 : Génération HTML ===\n";
+try {
+    $inputField = csrf::inputField('html-test');
+    echo "✅ Input field généré : " . htmlspecialchars($inputField) . "\n";
+
+    $metaTag = csrf::metaTag('ajax-test');
+    echo "✅ Meta tag généré : " . htmlspecialchars($metaTag) . "\n";
+} catch (Exception $e) {
+    echo "❌ Erreur : " . $e->getMessage() . "\n";
+}
+
+// Test 6 : Statistiques CSRF
+echo "\n=== TEST 6 : Statistiques CSRF ===\n";
+try {
+    // Générer quelques tokens
+    csrf::generateToken('form-a');
+    csrf::generateToken('form-b');
+    csrf::generateToken('form-c');
+
+    $stats = csrf::getStats();
+    echo "✅ Nombre de tokens actifs : " . $stats['count'] . "\n";
+
+    foreach ($stats['forms'] as $formName => $info) {
+        echo "   - $formName : âge = {$info['age']}s, créé le {$info['created_at']}\n";
+    }
+} catch (Exception $e) {
+    echo "❌ Erreur : " . $e->getMessage() . "\n";
+}
+
+// Test 7 : Nettoyage des tokens expirés
+echo "\n=== TEST 7 : Nettoyage tokens expirés ===\n";
+try {
+    // Tous les tokens ont été créés il y a moins d'1 seconde
+    $removed = csrf::cleanExpiredTokens(1); // Expiration après 1 seconde
+
+    sleep(2); // Attendre 2 secondes
+
+    $removed = csrf::cleanExpiredTokens(1);
+    echo "✅ Tokens expirés supprimés : $removed\n";
+} catch (Exception $e) {
+    echo "❌ Erreur : " . $e->getMessage() . "\n";
+}
+
+// Test 8 : Régénération ID de session
+echo "\n=== TEST 8 : Régénération ID de session ===\n";
+try {
+    $oldId = session_id();
+    $regenerated = secureSession::regenerateId();
+    $newId = session_id();
+
+    echo $regenerated ? "✅ Régénération réussie\n" : "❌ Échec régénération\n";
+    echo ($oldId !== $newId) ? "✅ L'ID a bien changé\n" : "❌ L'ID n'a pas changé (problème)\n";
+    echo "   Ancien : " . substr($oldId, 0, 10) . "...\n";
+    echo "   Nouveau : " . substr($newId, 0, 10) . "...\n";
+} catch (Exception $e) {
+    echo "❌ Erreur : " . $e->getMessage() . "\n";
+}
+
+// Test 9 : Timeout d'inactivité
+echo "\n=== TEST 9 : Timeout d'inactivité ===\n";
+try {
+    $isValid = secureSession::setInactivityTimeout(30); // 30 minutes
+    echo $isValid ? "✅ Timeout configuré (30 minutes)\n" : "❌ Échec configuration timeout\n";
+
+    // Vérifier via la méthode session
+    $checkTimeout = session::checkTimeout(30);
+    echo $checkTimeout ? "✅ Session active (dans le timeout)\n" : "❌ Session expirée\n";
+} catch (Exception $e) {
+    echo "❌ Erreur : " . $e->getMessage() . "\n";
+}
+
+// Test 10 : Variables de session
+echo "\n=== TEST 10 : Variables de session ===\n";
+try {
+    echo "Variables de sécurité présentes :\n";
+    echo isset($_SESSION['_session_created']) ? "✅ _session_created\n" : "❌ _session_created manquante\n";
+    echo isset($_SESSION['_session_ip']) ? "✅ _session_ip : {$_SESSION['_session_ip']}\n" : "❌ _session_ip manquante\n";
+    echo isset($_SESSION['_session_user_agent']) ? "✅ _session_user_agent\n" : "❌ _session_user_agent manquante\n";
+    echo isset($_SESSION['_session_last_activity']) ? "✅ _session_last_activity\n" : "❌ _session_last_activity manquante\n";
+} catch (Exception $e) {
+    echo "❌ Erreur : " . $e->getMessage() . "\n";
+}
+
+// Résumé final
+echo "\n" . str_repeat("=", 50) . "\n";
+echo "✅ Tests terminés avec succès !\n";
+echo "Les fonctionnalités de sécurité sont opérationnelles.\n";
+echo str_repeat("=", 50) . "\n";
+
+echo "\n⚠️  N'OUBLIEZ PAS DE SUPPRIMER CE FICHIER EN PRODUCTION !\n";

+ 3 - 0
public-events/.htaccess

@@ -1 +1,4 @@
 ErrorDocument 404 /404.php
+RewriteEngine On
+RewriteCond %{HTTPS} !=on
+RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

+ 12 - 12
public-events/home.php

@@ -1,14 +1,14 @@
-<?php 
-
-session_start();
+<?php
 
 require_once "../env.inc.php";
-require_once "../access.inc.php";
 require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
 
-if($_SERVER['HTTP_HOST'] == DOMAIN_EVENTS){
-    if(session::isConnect("salarie")) {
+secureSession::start();
+require_once "../access.inc.php";
+
+if ($_SERVER['HTTP_HOST'] == DOMAIN_EVENTS) {
+    if (session::isConnect("salarie")) {
         define("TITLE", "CSE Invent : Vos évènements");
         require_once DIR_PHP_VIEWS . "_events.head.php";
         require_once DIR_PHP_VIEWS . "_events.nav.php";
@@ -16,10 +16,10 @@ if($_SERVER['HTTP_HOST'] == DOMAIN_EVENTS){
         require_once DIR_PHP_VIEWS . "_events.foot.php";
 
         // Si émargement 
-        if(session::getValue("eventQRCode", "salarie") != ""){
+        if (session::getValue("eventQRCode", "salarie") != "") {
             $check = event::checkEvenementBySalarie(session::getValue("eventQRCode", "salarie"));
-            if($check["result"] == TRUE){ 
-                if(event::emargementEvenement(event::getIdEvenementByMd5(session::getValue("eventQRCode", "salarie")), session::getId("salarie"))){
+            if ($check["result"] == TRUE) {
+                if (event::emargementEvenement(event::getIdEvenementByMd5(session::getValue("eventQRCode", "salarie")), session::getId("salarie"))) {
                     echo "<script>
                         const data = {
                             evenement : '" . addslashes($check["evenement"]) . "', 
@@ -33,10 +33,10 @@ if($_SERVER['HTTP_HOST'] == DOMAIN_EVENTS){
                     </script>";
                 } else {
                     alert::recError("Nous n'avons pas pu vous émarger sur l'évènement");
-                } 
+                }
                 session::setValue(NULL, "eventQRCode", "salarie");
             } else {
-                    echo "<script>
+                echo "<script>
                         const data = {
                             evenement : '" . addslashes($check["evenement"]) . "', 
                             result : false, 
@@ -56,7 +56,7 @@ if($_SERVER['HTTP_HOST'] == DOMAIN_EVENTS){
         exit();
     }
 } elseif ($_SERVER['HTTP_HOST'] == DOMAIN_CONTROL) {
-    if(session::isConnect()) {
+    if (session::isConnect()) {
         define("TITLE", "CSE Invent : Controle d'évènement");
         require_once DIR_PHP_VIEWS . "_events.head.php";
         require_once DIR_PHP_VIEWS . "_events.nav.php";

+ 7 - 7
public-events/index.php

@@ -1,15 +1,15 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
-require_once "../access.inc.php";
 require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
 
-if($_SERVER['HTTP_HOST'] == DOMAIN_EVENTS){
-    if(session::isConnect("salarie")) {
-        if(core::ifGet("e")){
+secureSession::start();
+require_once "../access.inc.php";
+
+if ($_SERVER['HTTP_HOST'] == DOMAIN_EVENTS) {
+    if (session::isConnect("salarie")) {
+        if (core::ifGet("e")) {
             session::setValue(core::getGet("e"), "eventQRCode", "salarie");
         }
         header("Location: /home.php");
@@ -19,7 +19,7 @@ if($_SERVER['HTTP_HOST'] == DOMAIN_EVENTS){
         get::page("login");
     }
 } elseif ($_SERVER['HTTP_HOST'] == DOMAIN_CONTROL) {
-    if(session::isConnect()) {
+    if (session::isConnect()) {
         header("Location: /home.php");
         exit();
     } else {

+ 4 - 4
public-events/json.php

@@ -1,12 +1,12 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
-require_once "../access.inc.php";
 require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
+
+secureSession::start();
+require_once "../access.inc.php";
 require_once DIR_PHP_LAYOUTS . "events.session.php";
 
 get::json();
-get::jsonData();
+get::jsonData();

+ 8 - 8
public-events/qrcode.php

@@ -1,19 +1,19 @@
 <?php
 
-session_start();
-
 require_once "../env.inc.php";
-require_once "../access.inc.php";
-require_once "../conf.inc.php"; 
+require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
 
-header ('Content-Type: image/png');
+secureSession::start();
+require_once "../access.inc.php";
+
+header('Content-Type: image/png');
 
-if(core::ifGet("q")){
+if (core::ifGet("q")) {
     $link = core::base64_url_decode(core::getGet("q"));
 } else {
-    $link = "https://".DOMAIN_EVENTS;
+    $link = "https://" . DOMAIN_EVENTS;
 }
 
 $qrcode = new myQrcode();
-$qrcode->show($link);
+$qrcode->show($link);

+ 4 - 3
public-events/submit.php

@@ -1,10 +1,11 @@
 <?php
 
-session_start();
 require_once "../env.inc.php";
-require_once "../access.inc.php";
 require_once "../conf.inc.php";
 require_once DIR_PHP_LAYOUTS . "header.php";
-require_once DIR_PHP_LAYOUTS . "events.session.php"; 
+
+secureSession::start();
+require_once "../access.inc.php";
+require_once DIR_PHP_LAYOUTS . "events.session.php";
 
 get::submit();