fix(security): systematic output escaping with e() and video-card partial
This commit is contained in:
@@ -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); ?>"
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user