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 https://plausible.io; "; // PLAUSIBLE UPDATED // 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' https://plausible.io " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : ''); if ($isLocalDev) { $connectSrc .= " ws: wss:"; // WebSockets pour le dev } $csp .= "connect-src " . $connectSrc . "; "; // Médias : toujours autoriser 'self', Mastodon et PeerTube $mediaSrc = "'self'"; // Ajouter l'instance Mastodon (pour les médias stockés sur l'instance) if ($mastodonDomain) { $mediaSrc .= " " . $mastodonDomain; } // Ajouter PeerTube if ($peertubeDomain) { $mediaSrc .= " " . $peertubeDomain; } // Ajouter l'URL S3 Mastodon si configurée (pour les médias externalisés) if (defined('MASTODON_S3_MEDIA_URL') && !empty(MASTODON_S3_MEDIA_URL)) { $s3Parsed = parse_url(MASTODON_S3_MEDIA_URL); if ($s3Parsed && isset($s3Parsed['host'])) { $s3Domain = $s3Parsed['scheme'] . '://' . $s3Parsed['host']; $mediaSrc .= " " . $s3Domain; } } if ($isLocalDev) { $mediaSrc .= " https: http:"; } else { $mediaSrc .= " https:"; } $csp .= "media-src " . $mediaSrc . "; "; $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() { $host = $_SERVER['HTTP_HOST'] ?? ''; if (empty($host)) { return false; } $expectedOrigin = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $host; // Vérifier l'en-tête Origin si présent $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (!empty($origin)) { return $origin === $expectedOrigin; } // Si Origin est absent (requête same-origin), vérifier le Referer $referer = $_SERVER['HTTP_REFERER'] ?? ''; if (!empty($referer)) { return strpos($referer, $expectedOrigin) === 0; } // Accepter si ni Origin ni Referer (certains navigateurs/configs) // La protection CSRF reste active via le token return true; } ?>