2025-07-17 09:57:47 +04:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* Fonctions de sécurité pour la validation et l'assainissement des entrées
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Valide et assainit un ID de vidéo UUID
|
|
|
|
|
*
|
|
|
|
|
* @param string $id ID à valider
|
|
|
|
|
* @return string|false ID validé ou false si invalide
|
|
|
|
|
*/
|
|
|
|
|
function validateVideoId($id) {
|
|
|
|
|
if (empty($id)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Nettoyer l'entrée
|
|
|
|
|
$id = trim($id);
|
|
|
|
|
|
|
|
|
|
// Vérifier le format UUID (format PeerTube)
|
|
|
|
|
if (!preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i', $id)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Valide et assainit une requête de recherche
|
|
|
|
|
*
|
|
|
|
|
* @param string $query Requête à valider
|
|
|
|
|
* @return string|false Requête validée ou false si invalide
|
|
|
|
|
*/
|
|
|
|
|
function validateSearchQuery($query) {
|
|
|
|
|
if (empty($query)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Nettoyer l'entrée
|
|
|
|
|
$query = trim($query);
|
|
|
|
|
|
|
|
|
|
// Limiter la longueur
|
|
|
|
|
if (strlen($query) > 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() {
|
|
|
|
|
header('X-Frame-Options: SAMEORIGIN');
|
|
|
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
|
|
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
2025-09-29 18:58:14 +04:00
|
|
|
|
2026-05-18 17:49:23 +04:00
|
|
|
$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']
|
|
|
|
|
);
|
2025-09-29 18:58:14 +04:00
|
|
|
|
2026-05-18 17:49:23 +04:00
|
|
|
// Domaines des 5 plateformes sociales
|
|
|
|
|
$fbScripts = 'https://connect.facebook.net';
|
|
|
|
|
$fbFrames = 'https://www.facebook.com https://staticxx.facebook.com https://www.facebook.net';
|
|
|
|
|
$xScripts = 'https://platform.twitter.com';
|
|
|
|
|
$xFrames = 'https://platform.twitter.com https://syndication.twitter.com https://cdn.syndication.twimg.com';
|
|
|
|
|
$igScripts = 'https://www.instagram.com';
|
|
|
|
|
$igFrames = 'https://www.instagram.com';
|
|
|
|
|
$ttScripts = 'https://www.tiktok.com https://lf16-tiktok-web.ttwstatic.com';
|
|
|
|
|
$ttFrames = 'https://www.tiktok.com';
|
|
|
|
|
$ytImages = 'https://i.ytimg.com https://yt3.ggpht.com';
|
2025-09-29 18:58:14 +04:00
|
|
|
|
2026-05-18 17:49:23 +04:00
|
|
|
$csp = "default-src 'self'; ";
|
|
|
|
|
$csp .= "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; ";
|
|
|
|
|
$csp .= "font-src 'self' https://cdnjs.cloudflare.com; ";
|
|
|
|
|
$csp .= "script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://plausible.io "
|
|
|
|
|
. "{$fbScripts} {$xScripts} {$igScripts} {$ttScripts}; ";
|
|
|
|
|
$csp .= "img-src 'self' data: {$ytImages} https://www.facebook.com https://pbs.twimg.com https://abs.twimg.com"
|
|
|
|
|
. ($isLocalDev ? " https: http:" : " https:") . "; ";
|
|
|
|
|
$csp .= "frame-src 'self' {$fbFrames} {$xFrames} {$igFrames} {$ttFrames} https://www.youtube.com"
|
|
|
|
|
. ($isLocalDev ? " http:" : "") . "; ";
|
|
|
|
|
$csp .= "connect-src 'self' https://plausible.io https://www.googleapis.com https://www.youtube.com "
|
|
|
|
|
. "https://www.facebook.com https://graph.facebook.com https://connect.facebook.net "
|
|
|
|
|
. "https://platform.twitter.com https://syndication.twitter.com https://cdn.syndication.twimg.com https://api.twitter.com "
|
|
|
|
|
. "https://www.instagram.com https://www.tiktok.com"
|
|
|
|
|
. ($isLocalDev ? " ws: wss:" : "") . "; ";
|
|
|
|
|
$csp .= "media-src 'self' https:; ";
|
2025-07-17 09:57:47 +04:00
|
|
|
$csp .= "object-src 'none'; ";
|
|
|
|
|
$csp .= "base-uri 'self';";
|
2026-05-18 17:49:23 +04:00
|
|
|
|
2025-07-17 09:57:47 +04:00
|
|
|
header('Content-Security-Policy: ' . $csp);
|
2026-05-18 17:49:23 +04:00
|
|
|
|
2025-07-17 09:57:47 +04:00
|
|
|
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
|
2026-01-15 19:38:55 +04:00
|
|
|
*
|
2025-07-17 09:57:47 +04:00
|
|
|
* @return bool True si l'origine est valide
|
|
|
|
|
*/
|
|
|
|
|
function validateAjaxOrigin() {
|
|
|
|
|
$host = $_SERVER['HTTP_HOST'] ?? '';
|
2026-01-15 19:38:55 +04:00
|
|
|
|
|
|
|
|
if (empty($host)) {
|
2025-07-17 09:57:47 +04:00
|
|
|
return false;
|
|
|
|
|
}
|
2026-01-15 19:38:55 +04:00
|
|
|
|
2025-07-17 09:57:47 +04:00
|
|
|
$expectedOrigin = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $host;
|
2026-01-15 19:38:55 +04:00
|
|
|
|
|
|
|
|
// 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;
|
2025-07-17 09:57:47 +04:00
|
|
|
}
|
|
|
|
|
?>
|