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
+8 -3
View File
@@ -45,7 +45,7 @@ if ($heroType === 'none') {
</div>
<div class="hero-video-container">
<iframe
src="<?php echo PEERTUBE_URL; ?>/videos/embed/<?php echo $liveStream['id']; ?>?autoplay=1&muted=1"
src="<?php echo e(PEERTUBE_URL . '/videos/embed/' . $liveStream['id'] . '?autoplay=1&muted=1'); ?>"
frameborder="0"
allowfullscreen="allowfullscreen"
allow="autoplay; fullscreen"
@@ -64,7 +64,7 @@ if ($heroType === 'none') {
<i class="fas fa-user-circle" aria-hidden="true"></i>
</div>
<?php else: ?>
<img src="<?php echo $liveStream['channelAvatar']; ?>" alt="Avatar de la chaîne <?php echo htmlspecialchars($liveStream['channel']); ?>" class="channel-avatar">
<img src="<?php echo e($liveStream['channelAvatar']); ?>" alt="Avatar de la chaîne <?php echo htmlspecialchars($liveStream['channel']); ?>" class="channel-avatar">
<?php endif; ?>
<span class="channel-name"><?php echo htmlspecialchars($liveStream['channel']); ?></span>
</div>
@@ -82,7 +82,12 @@ if ($heroType === 'none') {
$bgImageStyle = 'background-image: url(\'' . htmlspecialchars(NEXT_LIVE_IMAGE) . '\');';
}
?>
<div class="hero-next-live" style="<?php echo $bgImageStyle; ?>" nonce="<?php echo getCspNonce(); ?>">
<?php if (!empty($bgImageStyle)): ?>
<style nonce="<?php echo getCspNonce(); ?>">
.hero-next-live { <?php echo $bgImageStyle; ?> }
</style>
<?php endif; ?>
<div class="hero-next-live">
<?php if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)): ?>
<div class="hero-next-live-image-container">
<img src="<?php echo htmlspecialchars(NEXT_LIVE_IMAGE); ?>"
+58
View File
@@ -0,0 +1,58 @@
<?php
/**
* Partial de rendu d'une carte vidéo.
*
* Centralise le balisage des cartes vidéo (accueil, catégories, recherche,
* endpoint AJAX « Voir plus ») afin de garantir un échappement systématique
* des données issues de l'API PeerTube (titres, chaînes, vignettes, avatars).
*/
/**
* Génère le HTML d'une carte vidéo, entièrement échappé.
*
* @param array $video Données formatées de la vidéo (voir formatVideosData())
* @return string HTML de la carte
*/
function renderVideoCard(array $video) {
$id = e($video['id'] ?? '');
$title = e($video['title'] ?? '');
$thumbnail = e($video['thumbnail'] ?? '');
$duration = e(formatDuration($video['duration'] ?? 0));
$channel = e($video['channel'] ?? '');
$channelAvatar = (string) ($video['channelAvatar'] ?? '');
$views = e(formatViewCount($video['views'] ?? 0));
$date = e(formatDate($video['date'] ?? ''));
ob_start();
?>
<article class="video-card" data-video-id="<?php echo $id; ?>">
<div class="video-thumbnail">
<img src="<?php echo $thumbnail; ?>" alt="<?php echo $title; ?>">
<div class="video-play-icon" aria-hidden="true">
<i class="fas fa-play-circle"></i>
</div>
<div class="video-duration"><?php echo $duration; ?></div>
</div>
<div class="video-info">
<h3 class="video-title"><?php echo $title; ?></h3>
<div class="video-channel">
<?php if ($channelAvatar === '' || strpos($channelAvatar, 'default-avatar') !== false): ?>
<div class="channel-avatar-placeholder">
<i class="fas fa-user-circle"></i>
</div>
<?php else: ?>
<img src="<?php echo e($channelAvatar); ?>" alt="<?php echo $channel; ?>" class="channel-avatar">
<?php endif; ?>
<span class="channel-name"><?php echo $channel; ?></span>
</div>
<div class="video-metadata">
<?php if (defined('SHOW_VIDEO_VIEWS') && SHOW_VIDEO_VIEWS): ?>
<span class="video-views"><i class="fas fa-eye"></i> <?php echo $views; ?> vues</span>
<?php endif; ?>
<span class="video-date"><i class="far fa-calendar-alt"></i> <?php echo $date; ?></span>
</div>
</div>
</article>
<?php
return ob_get_clean();
}
+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;
}
?>