258 lines
12 KiB
PHP
258 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Tests unitaires pour includes/security.php
|
|
* (et les validateurs d'URL de includes/config.php)
|
|
*/
|
|
|
|
// --- validateVideoId --------------------------------------------------------
|
|
|
|
assertEquals(
|
|
'9c5de94d-8e1a-4b3c-9d2e-1234567890ab',
|
|
validateVideoId('9c5de94d-8e1a-4b3c-9d2e-1234567890ab'),
|
|
'validateVideoId accepte un UUID valide'
|
|
);
|
|
assertEquals(
|
|
'9C5DE94D-8E1A-4B3C-9D2E-1234567890AB',
|
|
validateVideoId('9C5DE94D-8E1A-4B3C-9D2E-1234567890AB'),
|
|
'validateVideoId accepte un UUID en majuscules'
|
|
);
|
|
assertEquals(
|
|
'9c5de94d-8e1a-4b3c-9d2e-1234567890ab',
|
|
validateVideoId(' 9c5de94d-8e1a-4b3c-9d2e-1234567890ab '),
|
|
'validateVideoId nettoie les espaces autour de l\'UUID'
|
|
);
|
|
assertFalse(validateVideoId(''), 'validateVideoId refuse une chaîne vide');
|
|
assertFalse(validateVideoId('pas-un-uuid'), 'validateVideoId refuse une chaîne quelconque');
|
|
assertFalse(
|
|
validateVideoId('9c5de94d-8e1a-4b3c-1234567890ab'),
|
|
'validateVideoId refuse un UUID tronqué'
|
|
);
|
|
|
|
// --- validateSearchQuery ----------------------------------------------------
|
|
|
|
assertEquals('indépendance', validateSearchQuery('indépendance'), 'validateSearchQuery conserve les accents');
|
|
assertEquals(
|
|
'script',
|
|
validateSearchQuery('<script>'),
|
|
'validateSearchQuery supprime les chevrons'
|
|
);
|
|
assertEquals(
|
|
'recherche btest/b',
|
|
validateSearchQuery('recherche <b>test</b>'),
|
|
'validateSearchQuery supprime les caractères dangereux <>"\''
|
|
);
|
|
assertEquals('terme', validateSearchQuery(' terme '), 'validateSearchQuery nettoie les espaces');
|
|
assertFalse(validateSearchQuery(''), 'validateSearchQuery refuse une chaîne vide');
|
|
assertFalse(
|
|
validateSearchQuery(str_repeat('a', 201)),
|
|
'validateSearchQuery refuse plus de 200 caractères'
|
|
);
|
|
assertEquals(
|
|
str_repeat('a', 200),
|
|
validateSearchQuery(str_repeat('a', 200)),
|
|
'validateSearchQuery accepte exactement 200 caractères'
|
|
);
|
|
|
|
// --- validateCategoryId -----------------------------------------------------
|
|
|
|
assertEquals(1, validateCategoryId(1), 'validateCategoryId accepte la borne basse 1');
|
|
assertEquals(20, validateCategoryId(20), 'validateCategoryId accepte la borne haute 20');
|
|
assertEquals(5, validateCategoryId('5'), 'validateCategoryId convertit une chaîne numérique');
|
|
assertFalse(validateCategoryId(0), 'validateCategoryId refuse 0');
|
|
assertFalse(validateCategoryId(21), 'validateCategoryId refuse 21');
|
|
assertFalse(validateCategoryId(-3), 'validateCategoryId refuse un nombre négatif');
|
|
assertFalse(validateCategoryId('abc'), 'validateCategoryId refuse une chaîne non numérique');
|
|
|
|
// --- validatePageNumber -----------------------------------------------------
|
|
|
|
assertEquals(1, validatePageNumber(0), 'validatePageNumber remonte 0 à 1');
|
|
assertEquals(1, validatePageNumber(-5), 'validatePageNumber remonte un négatif à 1');
|
|
assertEquals(3, validatePageNumber(3), 'validatePageNumber conserve une page valide');
|
|
assertEquals(2, validatePageNumber('2'), 'validatePageNumber convertit une chaîne numérique');
|
|
assertEquals(1, validatePageNumber('abc'), 'validatePageNumber ramène une chaîne non numérique à 1');
|
|
|
|
// --- generateCSRFToken / validateCSRFToken ----------------------------------
|
|
|
|
$token = generateCSRFToken();
|
|
assertContains(':', $token, 'generateCSRFToken produit le format "timestamp:hash"');
|
|
|
|
[$timestamp, $hash] = explode(':', $token, 2);
|
|
assertTrue(ctype_digit($timestamp), 'generateCSRFToken : la partie timestamp est numérique');
|
|
assertEquals(
|
|
hash_hmac('sha256', $timestamp, CSRF_SECRET),
|
|
$hash,
|
|
'generateCSRFToken : le hash est un HMAC-SHA256 du timestamp avec CSRF_SECRET'
|
|
);
|
|
assertTrue(validateCSRFToken($token), 'validateCSRFToken accepte un token fraîchement généré');
|
|
|
|
assertFalse(validateCSRFToken(''), 'validateCSRFToken refuse une chaîne vide');
|
|
assertFalse(validateCSRFToken('sans-deux-points'), 'validateCSRFToken refuse un token sans séparateur');
|
|
assertFalse(validateCSRFToken('abc:' . $hash), 'validateCSRFToken refuse un timestamp non numérique');
|
|
assertFalse(
|
|
validateCSRFToken($timestamp . ':' . hash_hmac('sha256', $timestamp, 'autre-secret')),
|
|
'validateCSRFToken refuse un hash signé avec un autre secret'
|
|
);
|
|
assertFalse(
|
|
validateCSRFToken($timestamp . ':0' . $hash),
|
|
'validateCSRFToken refuse un hash modifié'
|
|
);
|
|
|
|
// Token expiré : horodatage d'il y a 2 heures (limite : 1 heure)
|
|
$oldTimestamp = (string) (time() - 7200);
|
|
$oldToken = $oldTimestamp . ':' . hash_hmac('sha256', $oldTimestamp, CSRF_SECRET);
|
|
assertFalse(validateCSRFToken($oldToken), 'validateCSRFToken refuse un token expiré (> 1 heure)');
|
|
|
|
// Timestamp trop loin dans le futur (l'écart absolu est également plafonné)
|
|
$futureTimestamp = (string) (time() + 7200);
|
|
$futureToken = $futureTimestamp . ':' . hash_hmac('sha256', $futureTimestamp, CSRF_SECRET);
|
|
assertFalse(validateCSRFToken($futureToken), 'validateCSRFToken refuse un timestamp trop futur');
|
|
|
|
// --- isValidPeerTubeUrl (includes/config.php) -------------------------------
|
|
|
|
assertTrue(isValidPeerTubeUrl('https://peertube.example.com'), 'isValidPeerTubeUrl accepte une URL HTTPS publique');
|
|
assertTrue(isValidPeerTubeUrl('http://peertube.example.com'), 'isValidPeerTubeUrl accepte HTTP (développement)');
|
|
assertTrue(isValidPeerTubeUrl('https://8.8.8.8'), 'isValidPeerTubeUrl accepte une IP publique');
|
|
assertFalse(isValidPeerTubeUrl('ftp://example.com'), 'isValidPeerTubeUrl refuse un schéma non HTTP(S)');
|
|
assertFalse(isValidPeerTubeUrl('pas-une-url'), 'isValidPeerTubeUrl refuse une chaîne mal formée');
|
|
assertFalse(isValidPeerTubeUrl('https://localhost'), 'isValidPeerTubeUrl refuse localhost');
|
|
assertFalse(isValidPeerTubeUrl('http://127.0.0.1'), 'isValidPeerTubeUrl refuse 127.0.0.1');
|
|
assertFalse(isValidPeerTubeUrl('http://192.168.1.1'), 'isValidPeerTubeUrl refuse une IP privée');
|
|
assertFalse(isValidPeerTubeUrl('http://10.0.0.5'), 'isValidPeerTubeUrl refuse une IP privée (10.x)');
|
|
assertFalse(
|
|
isValidPeerTubeUrl('https://metadata.google.internal'),
|
|
'isValidPeerTubeUrl refuse metadata.google.internal'
|
|
);
|
|
|
|
// --- isValidApiEndpoint (includes/config.php) -------------------------------
|
|
|
|
assertTrue(isValidApiEndpoint('videos'), 'isValidApiEndpoint accepte "videos"');
|
|
assertTrue(isValidApiEndpoint('videos/categories'), 'isValidApiEndpoint accepte "videos/categories"');
|
|
assertTrue(isValidApiEndpoint('search/videos'), 'isValidApiEndpoint accepte "search/videos"');
|
|
assertTrue(isValidApiEndpoint('accounts'), 'isValidApiEndpoint accepte "accounts"');
|
|
assertTrue(
|
|
isValidApiEndpoint('videos/9c5de94d-8e1a-4b3c-9d2e-1234567890ab'),
|
|
'isValidApiEndpoint accepte un endpoint vidéo dynamique'
|
|
);
|
|
assertTrue(
|
|
isValidApiEndpoint('videos/abc/comment-threads'),
|
|
'isValidApiEndpoint accepte les fils de commentaires'
|
|
);
|
|
assertTrue(
|
|
isValidApiEndpoint('accounts/annu_kute_ced/videos'),
|
|
'isValidApiEndpoint accepte les vidéos d\'un compte'
|
|
);
|
|
assertTrue(
|
|
isValidApiEndpoint('video-channels/annu_kute_ced/videos'),
|
|
'isValidApiEndpoint accepte les vidéos d\'une chaîne'
|
|
);
|
|
assertFalse(
|
|
isValidApiEndpoint('video-channels/annu_kute_ced'),
|
|
'isValidApiEndpoint refuse une chaîne sans sous-chemin /videos'
|
|
);
|
|
assertFalse(
|
|
isValidApiEndpoint('video-channels'),
|
|
'isValidApiEndpoint refuse "video-channels" seul'
|
|
);
|
|
assertFalse(isValidApiEndpoint('../config'), 'isValidApiEndpoint refuse le path traversal');
|
|
assertFalse(isValidApiEndpoint('videos/../x'), 'isValidApiEndpoint refuse ".." dans le chemin');
|
|
assertFalse(isValidApiEndpoint('videos//categories'), 'isValidApiEndpoint refuse un double slash');
|
|
assertFalse(isValidApiEndpoint('wp-admin'), 'isValidApiEndpoint refuse un endpoint hors liste blanche');
|
|
assertFalse(isValidApiEndpoint('videos.json'), 'isValidApiEndpoint refuse les caractères non autorisés (.)');
|
|
assertFalse(isValidApiEndpoint('videos?count=1'), 'isValidApiEndpoint refuse une query string');
|
|
assertFalse(isValidApiEndpoint(''), 'isValidApiEndpoint refuse une chaîne vide');
|
|
|
|
// --- getCspNonce ------------------------------------------------------------
|
|
|
|
$nonce = getCspNonce();
|
|
assertTrue(is_string($nonce) && $nonce !== '', 'getCspNonce retourne une chaîne non vide');
|
|
assertEquals(16, strlen(base64_decode($nonce, true)), 'getCspNonce est un base64 de 16 octets aléatoires');
|
|
assertEquals($nonce, getCspNonce(), 'getCspNonce retourne le même nonce durant toute la requête');
|
|
|
|
// --- getCsrfSecret ------------------------------------------------------------
|
|
|
|
assertEquals(
|
|
CSRF_SECRET,
|
|
getCsrfSecret(),
|
|
'getCsrfSecret retourne CSRF_SECRET quand il est configuré'
|
|
);
|
|
|
|
// Repli éphémère : impossible à tester dans ce processus (CSRF_SECRET est déjà
|
|
// défini par le bootstrap), on passe par un sous-processus PHP isolé dont la
|
|
// sortie d'erreur est redirigée vers stdout pour capter l'avertissement.
|
|
$securityFile = dirname(__DIR__, 2) . '/includes/security.php';
|
|
$snippet = <<<'PHP'
|
|
require $argv[1];
|
|
define('CSRF_SECRET', CSRF_SECRET_PLACEHOLDER);
|
|
$s1 = getCsrfSecret();
|
|
$s2 = getCsrfSecret();
|
|
echo 'len=' . strlen($s1) . "\n";
|
|
echo 'same=' . ($s1 === $s2 ? 'yes' : 'no') . "\n";
|
|
echo 'placeholder=' . ($s1 === CSRF_SECRET ? 'yes' : 'no') . "\n";
|
|
$token = generateCSRFToken();
|
|
echo 'token=' . (validateCSRFToken($token) ? 'valid' : 'invalid') . "\n";
|
|
PHP;
|
|
|
|
$cmd = escapeshellarg(PHP_BINARY)
|
|
. ' -d log_errors=1 -d error_log=/dev/stdout -r '
|
|
. escapeshellarg($snippet)
|
|
. ' ' . escapeshellarg($securityFile);
|
|
$fallbackOutput = function_exists('shell_exec') ? shell_exec($cmd) : null;
|
|
|
|
if ($fallbackOutput === null) {
|
|
test_record(false, 'getCsrfSecret : sous-processus de test du repli éphémère (shell_exec indisponible)');
|
|
} else {
|
|
assertContains(
|
|
'SECURITY CRITICAL',
|
|
$fallbackOutput,
|
|
'getCsrfSecret enregistre un avertissement critique avec la valeur par défaut'
|
|
);
|
|
assertContains(
|
|
'CSRF_SECRET',
|
|
$fallbackOutput,
|
|
'l\'avertissement critique mentionne CSRF_SECRET'
|
|
);
|
|
assertContains(
|
|
'len=64',
|
|
$fallbackOutput,
|
|
'le secret éphémère fait 64 caractères hexadécimaux (32 octets)'
|
|
);
|
|
assertContains(
|
|
'same=yes',
|
|
$fallbackOutput,
|
|
'le secret éphémère est stable durant tout le processus'
|
|
);
|
|
assertContains(
|
|
'placeholder=no',
|
|
$fallbackOutput,
|
|
'le secret éphémère diffère de la valeur par défaut'
|
|
);
|
|
assertContains(
|
|
'token=valid',
|
|
$fallbackOutput,
|
|
'un token signé avec le secret éphémère est validé dans le même processus'
|
|
);
|
|
}
|
|
|
|
// --- isValidRemoteUrl (includes/security.php) ---------------------------------
|
|
|
|
assertTrue(isValidRemoteUrl('https://peertube.example.com'), 'isValidRemoteUrl accepte une URL HTTPS publique');
|
|
assertTrue(isValidRemoteUrl('https://kute.o-k-i.net'), 'isValidRemoteUrl accepte une instance Castopod publique');
|
|
assertTrue(isValidRemoteUrl('https://mizik.o-k-i.net'), 'isValidRemoteUrl accepte une instance Funkwhale publique');
|
|
assertTrue(isValidRemoteUrl('http://castopod.example.com'), 'isValidRemoteUrl accepte HTTP (développement)');
|
|
assertFalse(isValidRemoteUrl('ftp://kute.o-k-i.net'), 'isValidRemoteUrl refuse un schéma non HTTP(S)');
|
|
assertFalse(isValidRemoteUrl('pas-une-url'), 'isValidRemoteUrl refuse une chaîne mal formée');
|
|
assertFalse(isValidRemoteUrl(''), 'isValidRemoteUrl refuse une chaîne vide');
|
|
assertFalse(isValidRemoteUrl('https://localhost'), 'isValidRemoteUrl refuse localhost');
|
|
assertFalse(isValidRemoteUrl('http://127.0.0.1'), 'isValidRemoteUrl refuse 127.0.0.1');
|
|
assertFalse(isValidRemoteUrl('http://192.168.1.1'), 'isValidRemoteUrl refuse une IP privée (192.168.x)');
|
|
assertFalse(isValidRemoteUrl('http://10.0.0.5'), 'isValidRemoteUrl refuse une IP privée (10.x)');
|
|
assertFalse(isValidRemoteUrl('http://172.16.0.1'), 'isValidRemoteUrl refuse une IP privée (172.16.x)');
|
|
assertFalse(
|
|
isValidRemoteUrl('http://169.254.169.254/latest/meta-data'),
|
|
'isValidRemoteUrl refuse l\'IP de métadonnées cloud (link-local)'
|
|
);
|
|
assertFalse(
|
|
isValidRemoteUrl('https://metadata.google.internal'),
|
|
'isValidRemoteUrl refuse metadata.google.internal'
|
|
);
|