From bbe807160e036135d0beb9a9a56517b66cc0b181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Thu, 17 Jul 2025 09:57:47 +0400 Subject: [PATCH] add security functions and HTTP headers --- includes/security.php | 278 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 includes/security.php diff --git a/includes/security.php b/includes/security.php new file mode 100644 index 0000000..14f7d52 --- /dev/null +++ b/includes/security.php @@ -0,0 +1,278 @@ + 200) { + return false; + } + + // Supprimer les caractères dangereux mais garder les caractères utiles pour la recherche + $query = preg_replace('/[<>"\']/', '', $query); + + return $query; +} + +/** + * Valide un numéro de page + * + * @param mixed $page Page à valider + * @return int Page validée (minimum 1) + */ +function validatePageNumber($page) { + $page = intval($page); + return max(1, $page); +} + +/** + * Valide un ID de catégorie + * + * @param mixed $categoryId ID de catégorie à valider + * @return int|false ID validé ou false si invalide + */ +function validateCategoryId($categoryId) { + $categoryId = intval($categoryId); + + // Les IDs de catégorie PeerTube sont entre 1 et 20 + if ($categoryId < 1 || $categoryId > 20) { + return false; + } + + return $categoryId; +} + +/** + * Valide et assainit un User-Agent + * + * @param string $userAgent User-Agent à valider + * @return bool True si valide + */ +function validateUserAgent($userAgent) { + if (empty($userAgent)) { + return false; + } + + // Bloquer les User-Agents suspects + $blockedPatterns = [ + '/curl/i', + '/wget/i', + '/python/i', + '/bot/i', + '/scanner/i', + '/sqlmap/i' + ]; + + foreach ($blockedPatterns as $pattern) { + if (preg_match($pattern, $userAgent)) { + return false; + } + } + + return true; +} + +/** + * Valide les en-têtes HTTP pour détecter les tentatives d'attaque + * + * @return bool True si les en-têtes sont sûrs + */ +function validateHttpHeaders() { + // Vérifier le User-Agent + $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + if (!validateUserAgent($userAgent)) { + error_log('SECURITY: Suspicious User-Agent detected: ' . $userAgent); + return false; + } + + // Vérifier les en-têtes suspects + $suspiciousHeaders = [ + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_REAL_IP', + 'HTTP_CLIENT_IP' + ]; + + foreach ($suspiciousHeaders as $header) { + if (isset($_SERVER[$header])) { + $value = $_SERVER[$header]; + // Bloquer les IPs privées dans les en-têtes de forwarding + if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) { + error_log('SECURITY: Suspicious IP in header ' . $header . ': ' . $value); + } + } + } + + return true; +} + +/** + * Génère un token CSRF sécurisé + * + * @return string Token CSRF + */ +function generateCSRFToken() { + // Démarrer la session seulement si les en-têtes n'ont pas été envoyés + if (session_status() === PHP_SESSION_NONE && !headers_sent()) { + session_start(); + } + + if (!isset($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + + return $_SESSION['csrf_token']; +} + +/** + * Valide un token CSRF + * + * @param string $token Token à valider + * @return bool True si le token est valide + */ +function validateCSRFToken($token) { + if (session_status() === PHP_SESSION_NONE && !headers_sent()) { + session_start(); + } + + if (!isset($_SESSION['csrf_token'])) { + return false; + } + + return hash_equals($_SESSION['csrf_token'], $token); +} + +/** + * Applique des en-têtes de sécurité HTTP + */ +function setSecurityHeaders() { + // Protection contre le clickjacking (permettre les iframes du même site) + header('X-Frame-Options: SAMEORIGIN'); + + // Protection contre le MIME sniffing + header('X-Content-Type-Options: nosniff'); + + // Protection XSS basique + header('X-XSS-Protection: 1; mode=block'); + + // Politique de référent + header('Referrer-Policy: strict-origin-when-cross-origin'); + + // Content Security Policy avec support Mastodon et PeerTube + $mastodonDomain = ''; + $peertubeDomain = ''; + + // Extraire le domaine Mastodon si configuré + if (defined('MASTODON_INSTANCE_URL')) { + $mastodonParsed = parse_url(MASTODON_INSTANCE_URL); + if ($mastodonParsed && isset($mastodonParsed['host'])) { + $mastodonDomain = $mastodonParsed['scheme'] . '://' . $mastodonParsed['host']; + } + } + + // Extraire le domaine PeerTube si configuré + if (defined('PEERTUBE_URL')) { + $peertubeParsed = parse_url(PEERTUBE_URL); + if ($peertubeParsed && isset($peertubeParsed['host'])) { + $peertubeDomain = $peertubeParsed['scheme'] . '://' . $peertubeParsed['host']; + } + } + + // Détecter si on est en développement local + $isLocalDev = in_array($_SERVER['HTTP_HOST'] ?? '', ['127.0.0.1:8080', '127.0.0.1:8001', 'localhost:8080', 'localhost:8001', '127.0.0.1', 'localhost']); + + $csp = "default-src 'self'; "; + $csp .= "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; "; + $csp .= "script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; "; + + // Images : autoriser les domaines externes plus HTTPS général en dev + $imgSrc = "'self' data: " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : ''); + if ($isLocalDev) { + $imgSrc .= " https: http:"; + } else { + $imgSrc .= " https:"; + } + $csp .= "img-src " . $imgSrc . "; "; + + $csp .= "font-src 'self' https://cdnjs.cloudflare.com; "; + + // Frames : autoriser PeerTube et HTTPS général + $frameSrc = "'self' " . ($peertubeDomain ? $peertubeDomain : ''); + if ($isLocalDev) { + $frameSrc .= " https: http:"; + } else { + $frameSrc .= " https:"; + } + $csp .= "frame-src " . $frameSrc . "; "; + + // Connexions : autoriser Mastodon et PeerTube + $connectSrc = "'self' " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : ''); + if ($isLocalDev) { + $connectSrc .= " ws: wss:"; // WebSockets pour le dev + } + $csp .= "connect-src " . $connectSrc . "; "; + + $csp .= "object-src 'none'; "; + $csp .= "base-uri 'self';"; + + header('Content-Security-Policy: ' . $csp); + + // HTTPS strict transport security (seulement si HTTPS) + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { + header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); + } +} + +/** + * Valide l'origine de la requête pour les requêtes AJAX + * + * @return bool True si l'origine est valide + */ +function validateAjaxOrigin() { + $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; + $host = $_SERVER['HTTP_HOST'] ?? ''; + + if (empty($origin) || empty($host)) { + return false; + } + + $expectedOrigin = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $host; + + return $origin === $expectedOrigin; +} +?> \ No newline at end of file