fix(security): systematic output escaping with e() and video-card partial

This commit is contained in:
2026-07-27 01:54:42 +04:00
parent 442c262539
commit a28ba8bfec
8 changed files with 300 additions and 185 deletions
+90 -2
View File
@@ -3,6 +3,20 @@
* Fonctions de sécurité pour la validation et l'assainissement des entrées
*/
/**
* Échappe une valeur pour une sortie HTML (texte ou attribut).
*
* Raccourci pour htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') :
* les guillemets simples et doubles sont encodés, ce qui rend la sortie
* sûre aussi bien dans le contenu que dans les attributs.
*
* @param mixed $value Valeur à échapper
* @return string Valeur échappée
*/
function e($value) {
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
/**
* Valide et assainit un ID de vidéo UUID
*
@@ -141,6 +155,44 @@ function validateHttpHeaders() {
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.
*/
if (!defined('CSRF_SECRET_PLACEHOLDER')) {
define('CSRF_SECRET_PLACEHOLDER', 'change-me-in-config-local-php');
}
/**
* Retourne le secret CSRF effectif utilisé pour signer les tokens.
*
* Si CSRF_SECRET est absent, vide ou vaut encore la valeur par défaut, un
* avertissement critique est enregistré et un secret éphémère propre au
* processus est généré (bin2hex(random_bytes(32))) : les tokens restent
* signés, mais sont invalidés à chaque redémarrage du processus PHP.
*
* @return string Secret CSRF effectif
*/
function getCsrfSecret() {
static $secret = null;
if ($secret !== null) {
return $secret;
}
if (defined('CSRF_SECRET') && CSRF_SECRET !== '' && CSRF_SECRET !== CSRF_SECRET_PLACEHOLDER) {
$secret = CSRF_SECRET;
return $secret;
}
error_log('SECURITY CRITICAL: CSRF_SECRET is not configured (default value in use). '
. 'An ephemeral per-process secret was generated: CSRF tokens will be invalidated '
. 'on every process restart. Set CSRF_SECRET in config.local.php (bin2hex(random_bytes(32))).');
$secret = bin2hex(random_bytes(32));
return $secret;
}
/**
* Génère un token CSRF stateless (HMAC + timestamp).
*
@@ -152,7 +204,7 @@ function validateHttpHeaders() {
*/
function generateCSRFToken() {
$timestamp = time();
$hash = hash_hmac('sha256', (string) $timestamp, CSRF_SECRET);
$hash = hash_hmac('sha256', (string) $timestamp, getCsrfSecret());
return $timestamp . ':' . $hash;
}
@@ -183,7 +235,7 @@ function validateCSRFToken($token) {
return false;
}
$expectedHash = hash_hmac('sha256', $timestamp, CSRF_SECRET);
$expectedHash = hash_hmac('sha256', $timestamp, getCsrfSecret());
return hash_equals($expectedHash, $hash);
}
@@ -347,4 +399,40 @@ function validateAjaxOrigin() {
return false;
}
/**
* Valide une URL distante (PeerTube, Castopod, Funkwhale…) pour prévenir
* les attaques SSRF avant tout appel sortant.
*
* @param string $url URL à valider
* @return bool True si l'URL est valide et sûre
*/
function isValidRemoteUrl($url) {
// Vérifier que l'URL est bien formée
$parsed = parse_url($url);
if (!$parsed || !isset($parsed['scheme']) || !isset($parsed['host'])) {
return false;
}
// Autoriser uniquement HTTPS (ou HTTP en développement)
if (!in_array($parsed['scheme'], ['https', 'http'])) {
return false;
}
// Bloquer les adresses IP privées et locales
$host = $parsed['host'];
if (filter_var($host, FILTER_VALIDATE_IP)) {
if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return false;
}
}
// Bloquer localhost et autres domaines dangereux
$blockedHosts = ['localhost', '127.0.0.1', '::1', '0.0.0.0', 'metadata.google.internal'];
if (in_array(strtolower($host), $blockedHosts)) {
return false;
}
return true;
}
?>