chore: remove dead code and unused legacy files
This commit is contained in:
+157
-150
@@ -92,69 +92,6 @@ function validateCategoryId($categoryId) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valeur par défaut livrée dans config.default.php : si CSRF_SECRET vaut
|
||||
* encore cette valeur, le secret n'a pas été configuré pour l'instance.
|
||||
@@ -252,6 +189,104 @@ function getCspNonce() {
|
||||
return $GLOBALS['csp_nonce'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait l'origine « scheme://host » d'une URL configurée (pour la CSP).
|
||||
*
|
||||
* @param string $url URL à analyser
|
||||
* @return string Origine normalisée, ou chaîne vide si l'URL est vide/invalide
|
||||
*/
|
||||
function cspOriginFromUrl($url) {
|
||||
if (empty($url)) {
|
||||
return '';
|
||||
}
|
||||
$parsed = parse_url($url);
|
||||
if (!$parsed || !isset($parsed['scheme'], $parsed['host'])) {
|
||||
return '';
|
||||
}
|
||||
return $parsed['scheme'] . '://' . $parsed['host'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit la Content Security Policy de la requête courante.
|
||||
*
|
||||
* img-src et media-src sont restreints aux domaines réellement utilisés :
|
||||
* PeerTube (vignettes), Mastodon (avatars/médias) et son S3 éventuel,
|
||||
* Castopod (pochettes/flux audio), Funkwhale (pochettes/flux audio) et
|
||||
* WordPress (images mises en avant). En développement local, HTTP(S) général
|
||||
* reste autorisé pour faciliter les tests avec du contenu fédéré.
|
||||
*
|
||||
* @param string $nonce Nonce CSP de la requête
|
||||
* @return string Politique CSP complète
|
||||
*/
|
||||
function buildContentSecurityPolicy($nonce) {
|
||||
$mastodonDomain = defined('MASTODON_INSTANCE_URL') ? cspOriginFromUrl(MASTODON_INSTANCE_URL) : '';
|
||||
$peertubeDomain = defined('PEERTUBE_URL') ? cspOriginFromUrl(PEERTUBE_URL) : '';
|
||||
$castopodDomain = (defined('CASTOPOD_ENABLED') && CASTOPOD_ENABLED && defined('CASTOPOD_URL'))
|
||||
? cspOriginFromUrl(CASTOPOD_URL) : '';
|
||||
$funkwhaleDomain = (defined('FUNKWHALE_ENABLED') && FUNKWHALE_ENABLED && defined('FUNKWHALE_URL'))
|
||||
? cspOriginFromUrl(FUNKWHALE_URL) : '';
|
||||
$wordpressDomain = defined('WORDPRESS_URL') ? cspOriginFromUrl(WORDPRESS_URL) : '';
|
||||
$s3Domain = defined('MASTODON_S3_MEDIA_URL') ? cspOriginFromUrl(MASTODON_S3_MEDIA_URL) : '';
|
||||
|
||||
// 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' 'nonce-{$nonce}' https://cdnjs.cloudflare.com; ";
|
||||
$csp .= "script-src 'self' 'nonce-{$nonce}' https://cdnjs.cloudflare.com https://plausible.io; ";
|
||||
|
||||
// Images : uniquement les services réellement affichés (https: général en dev uniquement)
|
||||
$imgSrc = "'self' data:";
|
||||
foreach ([$mastodonDomain, $peertubeDomain, $castopodDomain, $funkwhaleDomain, $wordpressDomain, $s3Domain] as $domain) {
|
||||
if ($domain !== '') {
|
||||
$imgSrc .= ' ' . $domain;
|
||||
}
|
||||
}
|
||||
if ($isLocalDev) {
|
||||
$imgSrc .= ' https: http:';
|
||||
}
|
||||
$csp .= "img-src " . $imgSrc . "; ";
|
||||
|
||||
$csp .= "font-src 'self' https://cdnjs.cloudflare.com; ";
|
||||
|
||||
// Frames : autoriser PeerTube uniquement
|
||||
$frameSrc = "'self'" . ($peertubeDomain !== '' ? ' ' . $peertubeDomain : '');
|
||||
if ($isLocalDev) {
|
||||
$frameSrc .= ' https: http:';
|
||||
}
|
||||
$csp .= "frame-src " . $frameSrc . "; ";
|
||||
|
||||
// Connexions : autoriser Mastodon, PeerTube et Plausible
|
||||
$connectSrc = "'self' https://plausible.io";
|
||||
foreach ([$mastodonDomain, $peertubeDomain] as $domain) {
|
||||
if ($domain !== '') {
|
||||
$connectSrc .= ' ' . $domain;
|
||||
}
|
||||
}
|
||||
if ($isLocalDev) {
|
||||
$connectSrc .= ' ws: wss:'; // WebSockets pour le dev
|
||||
}
|
||||
$csp .= "connect-src " . $connectSrc . "; ";
|
||||
|
||||
// Médias : flux audio Castopod/Funkwhale, médias Mastodon (instance ou S3)
|
||||
$mediaSrc = "'self'";
|
||||
foreach ([$mastodonDomain, $peertubeDomain, $castopodDomain, $funkwhaleDomain, $s3Domain] as $domain) {
|
||||
if ($domain !== '') {
|
||||
$mediaSrc .= ' ' . $domain;
|
||||
}
|
||||
}
|
||||
if ($isLocalDev) {
|
||||
$mediaSrc .= ' https: http:';
|
||||
}
|
||||
$csp .= "media-src " . $mediaSrc . "; ";
|
||||
|
||||
$csp .= "object-src 'none'; ";
|
||||
$csp .= "base-uri 'self'; ";
|
||||
$csp .= "frame-ancestors 'self';";
|
||||
|
||||
return $csp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique des en-têtes de sécurité HTTP
|
||||
*/
|
||||
@@ -262,8 +297,8 @@ function setSecurityHeaders() {
|
||||
// Protection contre le MIME sniffing
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
// Protection XSS basique
|
||||
header('X-XSS-Protection: 1; mode=block');
|
||||
// X-XSS-Protection volontairement absent : en-tête obsolète, supplanté par
|
||||
// la CSP et pouvant introduire des vulnérabilités dans les anciens navigateurs.
|
||||
|
||||
// Politique de référent
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
@@ -277,91 +312,8 @@ function setSecurityHeaders() {
|
||||
// Permissions Policy (feature policy)
|
||||
header('Permissions-Policy: accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(self), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()');
|
||||
|
||||
// Content Security Policy avec support Mastodon et PeerTube
|
||||
$nonce = getCspNonce();
|
||||
$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' 'nonce-{$nonce}' https://cdnjs.cloudflare.com; ";
|
||||
$csp .= "script-src 'self' 'nonce-{$nonce}' https://cdnjs.cloudflare.com https://plausible.io; ";
|
||||
|
||||
// Images : autoriser les domaines connus plus HTTPS général pour le contenu fédéré
|
||||
$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 uniquement
|
||||
$frameSrc = "'self' " . ($peertubeDomain ? $peertubeDomain : '');
|
||||
if ($isLocalDev) {
|
||||
$frameSrc .= " https: http:";
|
||||
}
|
||||
$csp .= "frame-src " . $frameSrc . "; ";
|
||||
|
||||
// Connexions : autoriser Mastodon, PeerTube et Plausible
|
||||
$connectSrc = "'self' https://plausible.io " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : '');
|
||||
if ($isLocalDev) {
|
||||
$connectSrc .= " ws: wss:"; // WebSockets pour le dev
|
||||
}
|
||||
$csp .= "connect-src " . $connectSrc . "; ";
|
||||
|
||||
// Médias : autoriser 'self', Mastodon, PeerTube et S3 Mastodon
|
||||
$mediaSrc = "'self'";
|
||||
|
||||
if ($mastodonDomain) {
|
||||
$mediaSrc .= " " . $mastodonDomain;
|
||||
}
|
||||
|
||||
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'; ";
|
||||
$csp .= "frame-ancestors 'self';";
|
||||
|
||||
header('Content-Security-Policy: ' . $csp);
|
||||
// Content Security Policy (domaines réellement utilisés uniquement)
|
||||
header('Content-Security-Policy: ' . buildContentSecurityPolicy(getCspNonce()));
|
||||
|
||||
// HTTPS strict transport security (seulement si HTTPS)
|
||||
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
|
||||
@@ -400,6 +352,61 @@ function validateAjaxOrigin() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limitation de débit simple par identifiant (typiquement l'IP cliente).
|
||||
*
|
||||
* Fenêtre fixe stockée dans un fichier par identifiant (cache/rate-limit/),
|
||||
* verrouillée par flock() pour rester cohérente entre requêtes concurrentes.
|
||||
* En cas d'indisponibilité du stockage, la requête est autorisée (fail-open) :
|
||||
* l'endpoint reste protégé par les gardes AJAX, Origin et CSRF.
|
||||
*
|
||||
* @param string $identifier Identifiant du client (ex. REMOTE_ADDR)
|
||||
* @param int $maxRequests Nombre maximal de requêtes dans la fenêtre
|
||||
* @param int $windowSeconds Durée de la fenêtre en secondes
|
||||
* @param string|null $dir Répertoire de stockage (surtout pour les tests)
|
||||
* @return bool True si la requête est autorisée, false si la limite est atteinte
|
||||
*/
|
||||
function checkRateLimit($identifier, $maxRequests = 30, $windowSeconds = 60, $dir = null) {
|
||||
$dir = $dir ?? (__DIR__ . '/../cache/rate-limit');
|
||||
if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
|
||||
error_log('SECURITY: rate limit storage unavailable: ' . $dir);
|
||||
return true;
|
||||
}
|
||||
|
||||
$file = $dir . '/rl_' . hash('sha256', (string) $identifier) . '.json';
|
||||
$now = time();
|
||||
|
||||
$handle = fopen($file, 'c+');
|
||||
if ($handle === false) {
|
||||
error_log('SECURITY: rate limit file unavailable: ' . $file);
|
||||
return true;
|
||||
}
|
||||
|
||||
$allowed = true;
|
||||
if (flock($handle, LOCK_EX)) {
|
||||
$raw = stream_get_contents($handle);
|
||||
$state = $raw !== false ? json_decode($raw, true) : null;
|
||||
|
||||
if (!is_array($state) || !isset($state['reset']) || $now >= $state['reset']) {
|
||||
$state = ['count' => 0, 'reset' => $now + $windowSeconds];
|
||||
}
|
||||
|
||||
$state['count']++;
|
||||
if ($state['count'] > $maxRequests) {
|
||||
$allowed = false;
|
||||
}
|
||||
|
||||
rewind($handle);
|
||||
ftruncate($handle, 0);
|
||||
fwrite($handle, json_encode($state));
|
||||
fflush($handle);
|
||||
flock($handle, LOCK_UN);
|
||||
}
|
||||
fclose($handle);
|
||||
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide une URL distante (PeerTube, Castopod, Funkwhale…) pour prévenir
|
||||
* les attaques SSRF avant tout appel sortant.
|
||||
|
||||
Reference in New Issue
Block a user