fix(security): systematic output escaping with e() and video-card partial
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests unitaires pour le helper e() (includes/security.php)
|
||||
* et le partial renderVideoCard() (includes/partials/video-card.php)
|
||||
*
|
||||
* Non-régression XSS (SEC-1) : les données de l'API PeerTube (titres,
|
||||
* chaînes, vignettes, avatars) ne doivent jamais ressortir telles quelles
|
||||
* dans le HTML d'une carte vidéo.
|
||||
*/
|
||||
|
||||
// --- e() ---------------------------------------------------------------------
|
||||
|
||||
assertEquals(
|
||||
'<script>alert(1)</script>',
|
||||
e('<script>alert(1)</script>'),
|
||||
'e() échappe les balises HTML'
|
||||
);
|
||||
assertEquals(
|
||||
'"guillemets" 'apostrophes'',
|
||||
e('"guillemets" \'apostrophes\''),
|
||||
'e() échappe guillemets et apostrophes (ENT_QUOTES)'
|
||||
);
|
||||
assertEquals('&', e('&'), 'e() échappe l\'esperluette');
|
||||
assertEquals('', e(null), 'e() convertit null en chaîne vide');
|
||||
assertEquals('42', e(42), 'e() convertit les nombres en chaîne');
|
||||
|
||||
// --- renderVideoCard : structure de base -------------------------------------
|
||||
|
||||
$video = [
|
||||
'id' => 'abc-123',
|
||||
'title' => 'Ma vidéo',
|
||||
'thumbnail' => 'https://videos.example/lazy/abc.jpg',
|
||||
'duration' => 125,
|
||||
'channel' => 'Ma chaîne',
|
||||
'channelAvatar' => 'https://videos.example/avatars/a.png',
|
||||
'views' => 42,
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$html = renderVideoCard($video);
|
||||
|
||||
assertContains('class="video-card"', $html, 'renderVideoCard génère une carte vidéo');
|
||||
assertContains('data-video-id="abc-123"', $html, 'renderVideoCard expose l\'identifiant vidéo');
|
||||
assertContains('src="https://videos.example/lazy/abc.jpg"', $html, 'renderVideoCard affiche la vignette');
|
||||
assertContains('<h3 class="video-title">Ma vidéo</h3>', $html, 'renderVideoCard affiche le titre');
|
||||
assertContains('<span class="channel-name">Ma chaîne</span>', $html, 'renderVideoCard affiche la chaîne');
|
||||
assertContains('class="channel-avatar"', $html, 'renderVideoCard affiche l\'avatar personnalisé');
|
||||
assertContains('2:05', $html, 'renderVideoCard affiche la durée formatée');
|
||||
|
||||
// --- renderVideoCard : avatar par défaut --------------------------------------
|
||||
|
||||
$videoDefaultAvatar = $video;
|
||||
$videoDefaultAvatar['channelAvatar'] = 'img/default-avatar.png';
|
||||
$htmlDefault = renderVideoCard($videoDefaultAvatar);
|
||||
|
||||
assertContains('channel-avatar-placeholder', $htmlDefault, 'renderVideoCard utilise un placeholder pour l\'avatar par défaut');
|
||||
assertNotContains('class="channel-avatar"', $htmlDefault, 'renderVideoCard n\'affiche pas l\'image d\'avatar par défaut');
|
||||
|
||||
$videoEmptyAvatar = $video;
|
||||
$videoEmptyAvatar['channelAvatar'] = '';
|
||||
assertContains(
|
||||
'channel-avatar-placeholder',
|
||||
renderVideoCard($videoEmptyAvatar),
|
||||
'renderVideoCard utilise un placeholder si l\'avatar est vide'
|
||||
);
|
||||
|
||||
// --- renderVideoCard : échappement XSS (SEC-1) ---------------------------------
|
||||
|
||||
$maliciousTitle = '"><img src=x onerror=alert(1)><script>alert(2)</script>';
|
||||
$maliciousChannel = '<svg onload=alert(3)>';
|
||||
$maliciousThumbnail = 'https://videos.example/x.jpg" onerror="alert(4)';
|
||||
$maliciousAvatar = 'https://videos.example/a.png\' onerror=\'alert(5)';
|
||||
|
||||
$htmlXss = renderVideoCard([
|
||||
'id' => $maliciousTitle,
|
||||
'title' => $maliciousTitle,
|
||||
'thumbnail' => $maliciousThumbnail,
|
||||
'duration' => 60,
|
||||
'channel' => $maliciousChannel,
|
||||
'channelAvatar' => $maliciousAvatar,
|
||||
'views' => 1,
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
assertNotContains($maliciousTitle, $htmlXss, 'renderVideoCard ne ressort pas le titre brut');
|
||||
assertNotContains($maliciousChannel, $htmlXss, 'renderVideoCard ne ressort pas la chaîne brute');
|
||||
assertNotContains($maliciousThumbnail, $htmlXss, 'renderVideoCard ne ressort pas la vignette brute');
|
||||
assertNotContains($maliciousAvatar, $htmlXss, 'renderVideoCard ne ressort pas l\'avatar brut');
|
||||
assertNotContains('<script>', $htmlXss, 'renderVideoCard ne génère aucune balise script injectée');
|
||||
assertNotContains('onerror="', $htmlXss, 'renderVideoCard ne laisse passer aucun attribut d\'événement actif');
|
||||
|
||||
assertContains(
|
||||
htmlspecialchars($maliciousTitle, ENT_QUOTES, 'UTF-8'),
|
||||
$htmlXss,
|
||||
'renderVideoCard affiche le titre échappé'
|
||||
);
|
||||
assertContains(
|
||||
htmlspecialchars($maliciousChannel, ENT_QUOTES, 'UTF-8'),
|
||||
$htmlXss,
|
||||
'renderVideoCard affiche la chaîne échappée'
|
||||
);
|
||||
|
||||
// L'échappement ENT_QUOTES doit aussi protéger les attributs à apostrophes
|
||||
$htmlAttr = renderVideoCard([
|
||||
'id' => 'xyz',
|
||||
'title' => "Titre avec 'apostrophe'",
|
||||
'thumbnail' => 'https://videos.example/x.jpg',
|
||||
'duration' => 10,
|
||||
'channel' => 'Chaîne',
|
||||
'channelAvatar' => 'img/default-avatar.png',
|
||||
'views' => 0,
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
assertContains(''', $htmlAttr, 'renderVideoCard échappe les apostrophes dans les attributs');
|
||||
assertNotContains("'apostrophe'", $htmlAttr, 'renderVideoCard ne laisse pas d\'apostrophe brute');
|
||||
|
||||
// --- renderVideoCard : données incomplètes --------------------------------------
|
||||
|
||||
$htmlMinimal = renderVideoCard(['id' => 'solo-1']);
|
||||
|
||||
assertContains('class="video-card"', $htmlMinimal, 'renderVideoCard tolère une vidéo quasi vide');
|
||||
assertContains('data-video-id="solo-1"', $htmlMinimal, 'renderVideoCard conserve l\'id minimal');
|
||||
assertContains('channel-avatar-placeholder', $htmlMinimal, 'renderVideoCard met un placeholder sans avatar');
|
||||
Reference in New Issue
Block a user