feat: improve security headers and csp nonce handling

This commit is contained in:
2026-07-08 07:33:40 +04:00
parent 05d01fb7bc
commit 3e249bbc34
11 changed files with 62 additions and 42 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ if ($categoryId && isset($allCategories[$categoryId])) {
?> ?>
<!-- Script pour éviter le flash en mode sombre --> <!-- Script pour éviter le flash en mode sombre -->
<script> <script nonce="<?php echo getCspNonce(); ?>">
(function() { (function() {
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+1 -1
View File
@@ -57,7 +57,7 @@ setSecurityHeaders();
<meta name="twitter:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>"> <meta name="twitter:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
<!-- Configuration JavaScript --> <!-- Configuration JavaScript -->
<script> <script nonce="<?php echo getCspNonce(); ?>">
const COUNTDOWN_TARGET_DATE = '<?php echo defined('COUNTDOWN_TARGET_DATE') ? COUNTDOWN_TARGET_DATE : '2025-10-11 00:00:00'; ?>'; const COUNTDOWN_TARGET_DATE = '<?php echo defined('COUNTDOWN_TARGET_DATE') ? COUNTDOWN_TARGET_DATE : '2025-10-11 00:00:00'; ?>';
</script> </script>
</head> </head>
+3 -3
View File
@@ -50,7 +50,7 @@ $liveStream = getLiveStream();
<meta name="twitter:image" content="<?php echo getBaseUrl(); ?>/img/logo.png"> <meta name="twitter:image" content="<?php echo getBaseUrl(); ?>/img/logo.png">
<!-- Données structurées JSON-LD pour la page direct --> <!-- Données structurées JSON-LD pour la page direct -->
<script type="application/ld+json"> <script type="application/ld+json" nonce="<?php echo getCspNonce(); ?>">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "WebPage", "@type": "WebPage",
@@ -85,7 +85,7 @@ $liveStream = getLiveStream();
?> ?>
<!-- Script pour éviter le flash en mode sombre --> <!-- Script pour éviter le flash en mode sombre -->
<script> <script nonce="<?php echo getCspNonce(); ?>">
(function() { (function() {
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -163,7 +163,7 @@ $liveStream = getLiveStream();
$bgImageStyle = 'background-image: url(\'' . htmlspecialchars(NEXT_LIVE_IMAGE) . '\');'; $bgImageStyle = 'background-image: url(\'' . htmlspecialchars(NEXT_LIVE_IMAGE) . '\');';
} }
?> ?>
<div class="next-live-announcement" style="<?php echo $bgImageStyle; ?>"> <div class="next-live-announcement" style="<?php echo $bgImageStyle; ?>" nonce="<?php echo getCspNonce(); ?>">
<?php if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)): ?> <?php if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)): ?>
<div class="next-live-image-container"> <div class="next-live-image-container">
<img src="<?php echo htmlspecialchars(NEXT_LIVE_IMAGE); ?>" <img src="<?php echo htmlspecialchars(NEXT_LIVE_IMAGE); ?>"
+1 -1
View File
@@ -74,7 +74,7 @@
<button id="theme-toggle" class="icon-button" aria-label="Basculer entre mode clair et sombre" title="Changer le thème"> <button id="theme-toggle" class="icon-button" aria-label="Basculer entre mode clair et sombre" title="Changer le thème">
<i class="fas fa-sun" aria-hidden="true"></i> <i class="fas fa-sun" aria-hidden="true"></i>
</button> </button>
<button id="install-pwa" class="icon-button install-pwa-button" style="display: none;" aria-label="Installer l'application"> <button id="install-pwa" class="icon-button install-pwa-button" style="display: none;" nonce="<?php echo getCspNonce(); ?>" aria-label="Installer l'application">
<i class="fas fa-download" aria-hidden="true"></i> <i class="fas fa-download" aria-hidden="true"></i>
</button> </button>
<button class="mobile-menu-toggle" aria-expanded="false" aria-controls="mobile-menu" aria-label="Ouvrir le menu de navigation"> <button class="mobile-menu-toggle" aria-expanded="false" aria-controls="mobile-menu" aria-label="Ouvrir le menu de navigation">
+1 -1
View File
@@ -82,7 +82,7 @@ if ($heroType === 'none') {
$bgImageStyle = 'background-image: url(\'' . htmlspecialchars(NEXT_LIVE_IMAGE) . '\');'; $bgImageStyle = 'background-image: url(\'' . htmlspecialchars(NEXT_LIVE_IMAGE) . '\');';
} }
?> ?>
<div class="hero-next-live" style="<?php echo $bgImageStyle; ?>"> <div class="hero-next-live" style="<?php echo $bgImageStyle; ?>" nonce="<?php echo getCspNonce(); ?>">
<?php if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)): ?> <?php if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)): ?>
<div class="hero-next-live-image-container"> <div class="hero-next-live-image-container">
<img src="<?php echo htmlspecialchars(NEXT_LIVE_IMAGE); ?>" <img src="<?php echo htmlspecialchars(NEXT_LIVE_IMAGE); ?>"
+1 -1
View File
@@ -18,7 +18,7 @@ function addPWAHeaders() {
function addPWAScripts() { function addPWAScripts() {
?> ?>
<!-- PWA Service Worker --> <!-- PWA Service Worker -->
<script> <script nonce="<?php echo getCspNonce(); ?>">
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', function() { window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js') navigator.serviceWorker.register('/sw.js')
+31 -12
View File
@@ -177,6 +177,19 @@ function validateCSRFToken($token) {
return hash_equals($_SESSION['csrf_token'], $token); return hash_equals($_SESSION['csrf_token'], $token);
} }
/**
* Génère ou récupère le nonce CSP de la requête courante.
* Doit être appelé après setSecurityHeaders() pour que le nonce soit envoyé dans le header CSP.
*
* @return string Nonce CSP
*/
function getCspNonce() {
if (!isset($GLOBALS['csp_nonce'])) {
$GLOBALS['csp_nonce'] = base64_encode(random_bytes(16));
}
return $GLOBALS['csp_nonce'];
}
/** /**
* Applique des en-têtes de sécurité HTTP * Applique des en-têtes de sécurité HTTP
*/ */
@@ -193,7 +206,16 @@ function setSecurityHeaders() {
// Politique de référent // Politique de référent
header('Referrer-Policy: strict-origin-when-cross-origin'); header('Referrer-Policy: strict-origin-when-cross-origin');
// Isolation cross-origin
header('Cross-Origin-Resource-Policy: same-origin');
header('Cross-Origin-Opener-Policy: same-origin');
header('Cross-Origin-Embedder-Policy: require-corp');
// 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 // Content Security Policy avec support Mastodon et PeerTube
$nonce = getCspNonce();
$mastodonDomain = ''; $mastodonDomain = '';
$peertubeDomain = ''; $peertubeDomain = '';
@@ -217,10 +239,10 @@ function setSecurityHeaders() {
$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']); $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 = "default-src 'self'; ";
$csp .= "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; "; $csp .= "style-src 'self' 'nonce-{$nonce}' https://cdnjs.cloudflare.com; ";
$csp .= "script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://plausible.io; "; // PLAUSIBLE UPDATED $csp .= "script-src 'self' 'nonce-{$nonce}' https://cdnjs.cloudflare.com https://plausible.io; ";
// Images : autoriser les domaines externes plus HTTPS général en dev // Images : autoriser les domaines connus plus HTTPS général pour le contenu fédéré
$imgSrc = "'self' data: " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : ''); $imgSrc = "'self' data: " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : '');
if ($isLocalDev) { if ($isLocalDev) {
$imgSrc .= " https: http:"; $imgSrc .= " https: http:";
@@ -231,31 +253,27 @@ function setSecurityHeaders() {
$csp .= "font-src 'self' https://cdnjs.cloudflare.com; "; $csp .= "font-src 'self' https://cdnjs.cloudflare.com; ";
// Frames : autoriser PeerTube et HTTPS général // Frames : autoriser PeerTube uniquement
$frameSrc = "'self' " . ($peertubeDomain ? $peertubeDomain : ''); $frameSrc = "'self' " . ($peertubeDomain ? $peertubeDomain : '');
if ($isLocalDev) { if ($isLocalDev) {
$frameSrc .= " https: http:"; $frameSrc .= " https: http:";
} else {
$frameSrc .= " https:";
} }
$csp .= "frame-src " . $frameSrc . "; "; $csp .= "frame-src " . $frameSrc . "; ";
// Connexions : autoriser Mastodon et PeerTube // Connexions : autoriser Mastodon, PeerTube et Plausible
$connectSrc = "'self' https://plausible.io " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : ''); $connectSrc = "'self' https://plausible.io " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : '');
if ($isLocalDev) { if ($isLocalDev) {
$connectSrc .= " ws: wss:"; // WebSockets pour le dev $connectSrc .= " ws: wss:"; // WebSockets pour le dev
} }
$csp .= "connect-src " . $connectSrc . "; "; $csp .= "connect-src " . $connectSrc . "; ";
// Médias : toujours autoriser 'self', Mastodon et PeerTube // Médias : autoriser 'self', Mastodon, PeerTube et S3 Mastodon
$mediaSrc = "'self'"; $mediaSrc = "'self'";
// Ajouter l'instance Mastodon (pour les médias stockés sur l'instance)
if ($mastodonDomain) { if ($mastodonDomain) {
$mediaSrc .= " " . $mastodonDomain; $mediaSrc .= " " . $mastodonDomain;
} }
// Ajouter PeerTube
if ($peertubeDomain) { if ($peertubeDomain) {
$mediaSrc .= " " . $peertubeDomain; $mediaSrc .= " " . $peertubeDomain;
} }
@@ -277,13 +295,14 @@ function setSecurityHeaders() {
$csp .= "media-src " . $mediaSrc . "; "; $csp .= "media-src " . $mediaSrc . "; ";
$csp .= "object-src 'none'; "; $csp .= "object-src 'none'; ";
$csp .= "base-uri 'self';"; $csp .= "base-uri 'self'; ";
$csp .= "frame-ancestors 'self';";
header('Content-Security-Policy: ' . $csp); header('Content-Security-Policy: ' . $csp);
// HTTPS strict transport security (seulement si HTTPS) // HTTPS strict transport security (seulement si HTTPS)
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); header('Strict-Transport-Security: max-age=63072000; includeSubDomains; preload');
} }
} }
+2 -1
View File
@@ -313,7 +313,8 @@ function getBaseUrl() {
* @param string $jsonLd Données JSON-LD * @param string $jsonLd Données JSON-LD
*/ */
function outputJsonLd($jsonLd) { function outputJsonLd($jsonLd) {
echo '<script type="application/ld+json">' . "\n" . $jsonLd . "\n" . '</script>' . "\n"; $nonce = getCspNonce();
echo '<script type="application/ld+json" nonce="' . $nonce . '">' . "\n" . $jsonLd . "\n" . '</script>' . "\n";
} }
?> ?>
+2 -2
View File
@@ -81,7 +81,7 @@ setSecurityHeaders();
?> ?>
<!-- Script pour éviter le flash en mode sombre --> <!-- Script pour éviter le flash en mode sombre -->
<script> <script nonce="<?php echo getCspNonce(); ?>">
(function() { (function() {
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -679,7 +679,7 @@ setSecurityHeaders();
<script src="js/mastodon-config.php?v=<?php echo md5(MASTODON_INSTANCE_URL . MASTODON_DATE_FORMAT . MASTODON_BTN_SEE_MORE . MASTODON_BTN_RELOAD . MASTODON_MAX_POST_FETCH . MASTODON_MAX_POST_SHOW); ?>"></script> <script src="js/mastodon-config.php?v=<?php echo md5(MASTODON_INSTANCE_URL . MASTODON_DATE_FORMAT . MASTODON_BTN_SEE_MORE . MASTODON_BTN_RELOAD . MASTODON_MAX_POST_FETCH . MASTODON_MAX_POST_SHOW); ?>"></script>
<!-- PWA Service Worker --> <!-- PWA Service Worker -->
<script> <script nonce="<?php echo getCspNonce(); ?>">
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', function() { window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js') navigator.serviceWorker.register('/sw.js')
+1 -1
View File
@@ -105,7 +105,7 @@ if ($resultsCount > 0) {
<?php endif; ?> <?php endif; ?>
<!-- Script pour éviter le flash en mode sombre --> <!-- Script pour éviter le flash en mode sombre -->
<script> <script nonce="<?php echo getCspNonce(); ?>">
(function() { (function() {
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+3 -3
View File
@@ -167,7 +167,7 @@ if (empty($videoData) || isset($videoData['error'])) {
<?php endif; ?> <?php endif; ?>
<!-- Script pour éviter le flash en mode sombre --> <!-- Script pour éviter le flash en mode sombre -->
<script> <script nonce="<?php echo getCspNonce(); ?>">
(function() { (function() {
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -330,7 +330,7 @@ if (empty($videoData) || isset($videoData['error'])) {
<i class="fas fa-chevron-down" aria-hidden="true"></i> <i class="fas fa-chevron-down" aria-hidden="true"></i>
</button> </button>
</div> </div>
<div id="full-description" class="full-description" style="display: none;"> <div id="full-description" class="full-description" style="display: none;" nonce="<?php echo getCspNonce(); ?>">
<?php echo markdown_to_html($video['description']); ?> <?php echo markdown_to_html($video['description']); ?>
<button class="show-less-btn" aria-expanded="true" aria-controls="full-description"> <button class="show-less-btn" aria-expanded="true" aria-controls="full-description">
<span>Voir moins</span> <span>Voir moins</span>
@@ -566,7 +566,7 @@ if (empty($videoData) || isset($videoData['error'])) {
</div> </div>
<script src="js/main.js"></script> <script src="js/main.js"></script>
<script> <script nonce="<?php echo getCspNonce(); ?>">
// Script pour la modal de téléchargement // Script pour la modal de téléchargement
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Gestion de la description tronquée // Gestion de la description tronquée