security: SRI Font Awesome, CSP restrictions, remove X-XSS-Protection, rate limiting

This commit is contained in:
2026-07-27 09:15:20 +04:00
parent ddd82fdff6
commit 7cbb7334f0
11 changed files with 186 additions and 135 deletions
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* Tests unitaires pour checkRateLimit() (includes/security.php)
*/
$rlDir = sys_get_temp_dir() . '/annu-kute-rl-test-' . getmypid();
// Nettoyage d'une éventuelle exécution précédente
foreach (glob($rlDir . '/rl_*.json') ?: [] as $oldFile) {
unlink($oldFile);
}
// --- Limite de base -----------------------------------------------------------
$ip = '203.0.113.10';
assertTrue(checkRateLimit($ip, 3, 60, $rlDir), 'rate limit : la 1re requête est autorisée');
assertTrue(checkRateLimit($ip, 3, 60, $rlDir), 'rate limit : la 2e requête est autorisée');
assertTrue(checkRateLimit($ip, 3, 60, $rlDir), 'rate limit : la 3e requête est autorisée');
assertFalse(checkRateLimit($ip, 3, 60, $rlDir), 'rate limit : la 4e requête est refusée (limite 3)');
assertFalse(checkRateLimit($ip, 3, 60, $rlDir), 'rate limit : une 5e requête reste refusée');
// --- Indépendance des identifiants --------------------------------------------
assertTrue(
checkRateLimit('203.0.113.99', 3, 60, $rlDir),
'rate limit : un autre identifiant a son propre compteur'
);
assertTrue(
checkRateLimit($ip, 3, 60, $rlDir . '/autre'),
'rate limit : un autre répertoire de stockage a son propre compteur'
);
// --- Réinitialisation de la fenêtre -------------------------------------------
// Forcer l'expiration de la fenêtre en réécrivant le fichier d'état
$stateFile = $rlDir . '/rl_' . hash('sha256', $ip) . '.json';
assertTrue(file_exists($stateFile), 'rate limit : le fichier d\'état existe');
file_put_contents($stateFile, json_encode(['count' => 3, 'reset' => time() - 1]));
assertTrue(
checkRateLimit($ip, 3, 60, $rlDir),
'rate limit : le compteur repart à zéro après expiration de la fenêtre'
);
// --- Fail-open si le stockage est indisponible ---------------------------------
// Un chemin qui est un fichier (pas un répertoire) : mkdir doit échouer
$notADir = $rlDir . '/fichier-bloquant';
file_put_contents($notADir, 'x');
assertTrue(
@checkRateLimit('198.51.100.5', 1, 60, $notADir),
'rate limit : fail-open si le répertoire de stockage est indisponible'
);
// --- Nettoyage -----------------------------------------------------------------
foreach ([$rlDir, $rlDir . '/autre'] as $dir) {
foreach (glob($dir . '/rl_*.json') ?: [] as $file) {
unlink($file);
}
if (is_dir($dir)) {
rmdir($dir);
}
}
unlink($notADir);
+82
View File
@@ -255,3 +255,85 @@ assertFalse(
isValidRemoteUrl('https://metadata.google.internal'),
'isValidRemoteUrl refuse metadata.google.internal'
);
// --- cspOriginFromUrl ---------------------------------------------------------
assertEquals(
'https://peertube.example.com',
cspOriginFromUrl('https://peertube.example.com'),
'cspOriginFromUrl extrait l\'origine d\'une URL simple'
);
assertEquals(
'https://peertube.example.com',
cspOriginFromUrl('https://peertube.example.com/chemin/page?x=1'),
'cspOriginFromUrl ignore le chemin et la query string'
);
assertEquals(
'http://127.0.0.1',
cspOriginFromUrl('http://127.0.0.1'),
'cspOriginFromUrl conserve le schéma http'
);
assertEquals('', cspOriginFromUrl(''), 'cspOriginFromUrl refuse une chaîne vide');
assertEquals('', cspOriginFromUrl('pas-une-url'), 'cspOriginFromUrl refuse une URL mal formée');
// --- buildContentSecurityPolicy ------------------------------------------------
/**
* Extrait la valeur d'une directive CSP pour les assertions
*/
function csp_directive($csp, $name) {
if (preg_match('/(?:^| )' . preg_quote($name, '/') . ' ([^;]+)/', $csp, $m)) {
return $m[1];
}
return null;
}
$testNonce = 'nonce-de-test';
$csp = buildContentSecurityPolicy($testNonce);
assertContains(
"'nonce-{$testNonce}",
csp_directive($csp, 'script-src'),
'CSP : script-src porte le nonce de la requête'
);
assertContains(
"'nonce-{$testNonce}",
csp_directive($csp, 'style-src'),
'CSP : style-src porte le nonce de la requête'
);
// Domaines réellement utilisés présents
$imgSrc = csp_directive($csp, 'img-src');
$mediaSrc = csp_directive($csp, 'media-src');
assertContains(cspOriginFromUrl(PEERTUBE_URL), $imgSrc, 'CSP : img-src autorise le domaine PeerTube');
assertContains(cspOriginFromUrl(MASTODON_INSTANCE_URL), $imgSrc, 'CSP : img-src autorise le domaine Mastodon');
assertContains(cspOriginFromUrl(PEERTUBE_URL), $mediaSrc, 'CSP : media-src autorise le domaine PeerTube');
assertContains(cspOriginFromUrl(CASTOPOD_URL), $mediaSrc, 'CSP : media-src autorise le domaine Castopod');
assertContains(cspOriginFromUrl(CASTOPOD_URL), $imgSrc, 'CSP : img-src autorise les pochettes Castopod');
// Pas de joker https:/http: hors développement local (HTTP_HOST absent en CLI)
assertTrue(
preg_match('/(^|\s)https?:($|\s)/', $imgSrc) === 0,
'CSP : img-src ne contient pas de joker https: en production'
);
assertTrue(
preg_match('/(^|\s)https?:($|\s)/', $mediaSrc) === 0,
'CSP : media-src ne contient pas de joker https: en production'
);
// Directives de verrouillage toujours présentes
assertContains("object-src 'none'", $csp, 'CSP : object-src none est présent');
assertContains("frame-ancestors 'self'", $csp, 'CSP : frame-ancestors self est présent');
// En développement local, le joker HTTP(S) est réintroduit pour le contenu fédéré
$_SERVER['HTTP_HOST'] = 'localhost:8080';
$cspDev = buildContentSecurityPolicy($testNonce);
assertTrue(
preg_match('/(^|\s)https?:($|\s)/', csp_directive($cspDev, 'img-src')) === 1,
'CSP : img-src contient le joker https: en développement local'
);
assertTrue(
preg_match('/(^|\s)https?:($|\s)/', csp_directive($cspDev, 'media-src')) === 1,
'CSP : media-src contient le joker https: en développement local'
);
unset($_SERVER['HTTP_HOST']);