true, 'xframe' => true, 'xcontent' => true, 'xss' => true, 'referrer' => true, 'hsts' => true, 'permissions' => true ]; $options = array_merge($defaults, $options); // Ne pas envoyer si les headers sont déjà envoyés if (headers_sent()) { return; } if ($options['xframe']) { self::setXFrameOptions(); } if ($options['xcontent']) { self::setXContentTypeOptions(); } if ($options['xss']) { self::setXSSProtection(); } if ($options['referrer']) { self::setReferrerPolicy(); } if ($options['hsts'] && self::isHTTPS()) { self::setHSTS(); } if ($options['permissions']) { self::setPermissionsPolicy(); } if ($options['csp']) { self::setCSP(); } } /** * Applique les headers spécifiques pour la page de login */ public static function applyForLogin(): void { if (headers_sent()) { return; } // Headers de base self::setXFrameOptions('DENY'); // Plus strict pour le login self::setXContentTypeOptions(); self::setXSSProtection(); self::setReferrerPolicy('no-referrer'); if (self::isHTTPS()) { self::setHSTS(); } // CSP strict pour le login self::setLoginCSP(); // Cache control - Ne pas mettre en cache la page de login self::setNoCache(); } /** * X-Frame-Options : Empêche le clickjacking * * @param string $value DENY, SAMEORIGIN, ou ALLOW-FROM uri */ public static function setXFrameOptions(string $value = 'SAMEORIGIN'): void { header("X-Frame-Options: $value"); } /** * X-Content-Type-Options : Empêche le MIME sniffing */ public static function setXContentTypeOptions(): void { header('X-Content-Type-Options: nosniff'); } /** * X-XSS-Protection : Protection XSS du navigateur (legacy) */ public static function setXSSProtection(): void { header('X-XSS-Protection: 1; mode=block'); } /** * Referrer-Policy : Contrôle les informations de referrer * * @param string $policy Politique de referrer */ public static function setReferrerPolicy(string $policy = 'strict-origin-when-cross-origin'): void { header("Referrer-Policy: $policy"); } /** * Strict-Transport-Security : Force HTTPS * * @param int $maxAge Durée en secondes (défaut: 1 an) * @param bool $includeSubDomains Inclure les sous-domaines */ public static function setHSTS(int $maxAge = 31536000, bool $includeSubDomains = true): void { $header = "Strict-Transport-Security: max-age=$maxAge"; if ($includeSubDomains) { $header .= '; includeSubDomains'; } header($header); } /** * Permissions-Policy : Contrôle l'accès aux APIs du navigateur */ public static function setPermissionsPolicy(): void { header("Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()"); } /** * Content-Security-Policy : Politique de sécurité du contenu * * @param array $directives Directives CSP personnalisées */ public static function setCSP(array $directives = []): void { $defaults = [ "default-src" => "'self'", "script-src" => "'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com", "style-src" => "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com", "font-src" => "'self' https://fonts.gstatic.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com", "img-src" => "'self' data: https:", "connect-src" => "'self'", "frame-ancestors" => "'self'", "form-action" => "'self'", "base-uri" => "'self'" ]; $directives = array_merge($defaults, $directives); $csp = []; foreach ($directives as $directive => $value) { $csp[] = "$directive $value"; } header("Content-Security-Policy: " . implode('; ', $csp)); } /** * CSP strict pour la page de login */ public static function setLoginCSP(): void { $nonce = self::generateNonce(); $directives = [ "default-src" => "'self'", "script-src" => "'self' 'nonce-{$nonce}' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com", "style-src" => "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com", "font-src" => "'self' https://fonts.gstatic.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com", "img-src" => "'self' data:", "connect-src" => "'self'", "frame-ancestors" => "'none'", "form-action" => "'self'", "base-uri" => "'self'", "object-src" => "'none'" ]; $csp = []; foreach ($directives as $directive => $value) { $csp[] = "$directive $value"; } header("Content-Security-Policy: " . implode('; ', $csp)); } /** * Headers pour désactiver le cache */ public static function setNoCache(): void { header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Cache-Control: post-check=0, pre-check=0', false); header('Pragma: no-cache'); header('Expires: Thu, 01 Jan 1970 00:00:00 GMT'); } /** * Génère un nonce pour CSP * * @return string Nonce encodé en base64 */ public static function generateNonce(): string { if (!isset($_SESSION['csp_nonce'])) { $_SESSION['csp_nonce'] = base64_encode(random_bytes(16)); } return $_SESSION['csp_nonce']; } /** * Retourne le nonce actuel pour l'utiliser dans les scripts inline * * @return string Nonce pour attribut nonce="" */ public static function getNonce(): string { return $_SESSION['csp_nonce'] ?? self::generateNonce(); } /** * Vérifie si la connexion est HTTPS * * @return bool */ private static function isHTTPS(): bool { if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { return true; } if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { return true; } if (isset($_SERVER['HTTP_CF_VISITOR'])) { $visitor = json_decode($_SERVER['HTTP_CF_VISITOR'], true); if (isset($visitor['scheme']) && $visitor['scheme'] === 'https') { return true; } } if (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] === '443') { return true; } return false; } /** * Applique les headers de sécurité pour les API JSON */ public static function applyForAPI(): void { if (headers_sent()) { return; } header('Content-Type: application/json; charset=utf-8'); self::setXContentTypeOptions(); self::setNoCache(); // CORS si nécessaire // self::setCORS(); } /** * Configure CORS (Cross-Origin Resource Sharing) * * @param array $allowedOrigins Origines autorisées * @param array $allowedMethods Méthodes autorisées */ public static function setCORS(array $allowedOrigins = [], array $allowedMethods = ['GET', 'POST']): void { $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; // Si pas de restriction, n'autoriser que same-origin if (empty($allowedOrigins)) { return; } if (in_array($origin, $allowedOrigins) || in_array('*', $allowedOrigins)) { header("Access-Control-Allow-Origin: $origin"); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Methods: ' . implode(', ', $allowedMethods)); header('Access-Control-Allow-Headers: Content-Type, Authorization, X-CSRF-Token'); } } }