2
0

securityHeaders.class.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <?php
  2. /**
  3. * Classe securityHeaders
  4. *
  5. * Gère les en-têtes HTTP de sécurité
  6. */
  7. class securityHeaders
  8. {
  9. /**
  10. * Applique tous les headers de sécurité recommandés
  11. *
  12. * @param array $options Options de configuration
  13. */
  14. public static function apply(array $options = []): void
  15. {
  16. $defaults = [
  17. 'csp' => true,
  18. 'xframe' => true,
  19. 'xcontent' => true,
  20. 'xss' => true,
  21. 'referrer' => true,
  22. 'hsts' => true,
  23. 'permissions' => true
  24. ];
  25. $options = array_merge($defaults, $options);
  26. // Ne pas envoyer si les headers sont déjà envoyés
  27. if (headers_sent()) {
  28. return;
  29. }
  30. if ($options['xframe']) {
  31. self::setXFrameOptions();
  32. }
  33. if ($options['xcontent']) {
  34. self::setXContentTypeOptions();
  35. }
  36. if ($options['xss']) {
  37. self::setXSSProtection();
  38. }
  39. if ($options['referrer']) {
  40. self::setReferrerPolicy();
  41. }
  42. // HSTS uniquement en production (pas en DEV)
  43. if ($options['hsts'] && self::isHTTPS() && !self::isDevEnvironment()) {
  44. self::setHSTS();
  45. }
  46. if ($options['permissions']) {
  47. self::setPermissionsPolicy();
  48. }
  49. if ($options['csp']) {
  50. self::setCSP();
  51. }
  52. }
  53. /**
  54. * Applique les headers spécifiques pour la page de login
  55. */
  56. public static function applyForLogin(): void
  57. {
  58. if (headers_sent()) {
  59. return;
  60. }
  61. // Headers de base
  62. self::setXFrameOptions('DENY'); // Plus strict pour le login
  63. self::setXContentTypeOptions();
  64. self::setXSSProtection();
  65. self::setReferrerPolicy('no-referrer');
  66. // HSTS uniquement en production (pas en DEV pour éviter les problèmes de certificat local)
  67. if (self::isHTTPS() && !self::isDevEnvironment()) {
  68. self::setHSTS();
  69. }
  70. // CSP strict pour le login
  71. self::setLoginCSP();
  72. // Cache control - Ne pas mettre en cache la page de login
  73. self::setNoCache();
  74. }
  75. /**
  76. * X-Frame-Options : Empêche le clickjacking
  77. *
  78. * @param string $value DENY, SAMEORIGIN, ou ALLOW-FROM uri
  79. */
  80. public static function setXFrameOptions(string $value = 'SAMEORIGIN'): void
  81. {
  82. header("X-Frame-Options: $value");
  83. }
  84. /**
  85. * X-Content-Type-Options : Empêche le MIME sniffing
  86. */
  87. public static function setXContentTypeOptions(): void
  88. {
  89. header('X-Content-Type-Options: nosniff');
  90. }
  91. /**
  92. * X-XSS-Protection : Protection XSS du navigateur (legacy)
  93. */
  94. public static function setXSSProtection(): void
  95. {
  96. header('X-XSS-Protection: 1; mode=block');
  97. }
  98. /**
  99. * Referrer-Policy : Contrôle les informations de referrer
  100. *
  101. * @param string $policy Politique de referrer
  102. */
  103. public static function setReferrerPolicy(string $policy = 'strict-origin-when-cross-origin'): void
  104. {
  105. header("Referrer-Policy: $policy");
  106. }
  107. /**
  108. * Strict-Transport-Security : Force HTTPS
  109. *
  110. * @param int $maxAge Durée en secondes (défaut: 1 an)
  111. * @param bool $includeSubDomains Inclure les sous-domaines
  112. */
  113. public static function setHSTS(int $maxAge = 31536000, bool $includeSubDomains = true): void
  114. {
  115. $header = "Strict-Transport-Security: max-age=$maxAge";
  116. if ($includeSubDomains) {
  117. $header .= '; includeSubDomains';
  118. }
  119. header($header);
  120. }
  121. /**
  122. * Permissions-Policy : Contrôle l'accès aux APIs du navigateur
  123. */
  124. public static function setPermissionsPolicy(): void
  125. {
  126. header("Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()");
  127. }
  128. /**
  129. * Content-Security-Policy : Politique de sécurité du contenu
  130. *
  131. * @param array $directives Directives CSP personnalisées
  132. */
  133. public static function setCSP(array $directives = []): void
  134. {
  135. $defaults = [
  136. "default-src" => "'self'",
  137. "script-src" => "'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com",
  138. "style-src" => "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com",
  139. "font-src" => "'self' https://fonts.gstatic.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com",
  140. "img-src" => "'self' data: https:",
  141. "connect-src" => "'self'",
  142. "frame-ancestors" => "'self'",
  143. "form-action" => "'self'",
  144. "base-uri" => "'self'"
  145. ];
  146. $directives = array_merge($defaults, $directives);
  147. $csp = [];
  148. foreach ($directives as $directive => $value) {
  149. $csp[] = "$directive $value";
  150. }
  151. header("Content-Security-Policy: " . implode('; ', $csp));
  152. }
  153. /**
  154. * CSP strict pour la page de login
  155. */
  156. public static function setLoginCSP(): void
  157. {
  158. $nonce = self::generateNonce();
  159. $directives = [
  160. "default-src" => "'self'",
  161. "script-src" => "'self' 'nonce-{$nonce}' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com",
  162. "style-src" => "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com",
  163. "font-src" => "'self' https://fonts.gstatic.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com",
  164. "img-src" => "'self' data:",
  165. "connect-src" => "'self'",
  166. "frame-ancestors" => "'none'",
  167. "form-action" => "'self'",
  168. "base-uri" => "'self'",
  169. "object-src" => "'none'"
  170. ];
  171. $csp = [];
  172. foreach ($directives as $directive => $value) {
  173. $csp[] = "$directive $value";
  174. }
  175. header("Content-Security-Policy: " . implode('; ', $csp));
  176. }
  177. /**
  178. * Headers pour désactiver le cache
  179. */
  180. public static function setNoCache(): void
  181. {
  182. header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
  183. header('Cache-Control: post-check=0, pre-check=0', false);
  184. header('Pragma: no-cache');
  185. header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
  186. }
  187. /**
  188. * Génère un nonce pour CSP
  189. *
  190. * @return string Nonce encodé en base64
  191. */
  192. public static function generateNonce(): string
  193. {
  194. if (!isset($_SESSION['csp_nonce'])) {
  195. $_SESSION['csp_nonce'] = base64_encode(random_bytes(16));
  196. }
  197. return $_SESSION['csp_nonce'];
  198. }
  199. /**
  200. * Retourne le nonce actuel pour l'utiliser dans les scripts inline
  201. *
  202. * @return string Nonce pour attribut nonce=""
  203. */
  204. public static function getNonce(): string
  205. {
  206. return $_SESSION['csp_nonce'] ?? self::generateNonce();
  207. }
  208. /**
  209. * Vérifie si la connexion est HTTPS
  210. *
  211. * @return bool
  212. */
  213. private static function isHTTPS(): bool
  214. {
  215. if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
  216. return true;
  217. }
  218. if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
  219. return true;
  220. }
  221. if (isset($_SERVER['HTTP_CF_VISITOR'])) {
  222. $visitor = json_decode($_SERVER['HTTP_CF_VISITOR'], true);
  223. if (isset($visitor['scheme']) && $visitor['scheme'] === 'https') {
  224. return true;
  225. }
  226. }
  227. if (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] === '443') {
  228. return true;
  229. }
  230. return false;
  231. }
  232. /**
  233. * Vérifie si on est en environnement de développement
  234. *
  235. * @return bool
  236. */
  237. private static function isDevEnvironment(): bool
  238. {
  239. // Vérifier la constante ENVIRONNEMENT
  240. if (defined('ENVIRONNEMENT') && ENVIRONNEMENT === 'DEV') {
  241. return true;
  242. }
  243. // Vérifier les domaines locaux
  244. $host = $_SERVER['HTTP_HOST'] ?? '';
  245. if (strpos($host, 'local.') === 0 || strpos($host, 'localhost') !== false || strpos($host, '127.0.0.1') !== false) {
  246. return true;
  247. }
  248. return false;
  249. }
  250. /**
  251. * Applique les headers de sécurité pour les API JSON
  252. */
  253. public static function applyForAPI(): void
  254. {
  255. if (headers_sent()) {
  256. return;
  257. }
  258. header('Content-Type: application/json; charset=utf-8');
  259. self::setXContentTypeOptions();
  260. self::setNoCache();
  261. // CORS si nécessaire
  262. // self::setCORS();
  263. }
  264. /**
  265. * Configure CORS (Cross-Origin Resource Sharing)
  266. *
  267. * @param array $allowedOrigins Origines autorisées
  268. * @param array $allowedMethods Méthodes autorisées
  269. */
  270. public static function setCORS(array $allowedOrigins = [], array $allowedMethods = ['GET', 'POST']): void
  271. {
  272. $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
  273. // Si pas de restriction, n'autoriser que same-origin
  274. if (empty($allowedOrigins)) {
  275. return;
  276. }
  277. if (in_array($origin, $allowedOrigins) || in_array('*', $allowedOrigins)) {
  278. header("Access-Control-Allow-Origin: $origin");
  279. header('Access-Control-Allow-Credentials: true');
  280. header('Access-Control-Allow-Methods: ' . implode(', ', $allowedMethods));
  281. header('Access-Control-Allow-Headers: Content-Type, Authorization, X-CSRF-Token');
  282. }
  283. }
  284. }