fix(csrf): stateless HMAC token to survive page cache
Déploiement PROD / check (push) Successful in 3m26s
Déploiement PROD / deploy (push) Successful in 8s

This commit is contained in:
2026-07-26 18:44:05 +04:00
parent 9109068b0b
commit 5899dfb856
4 changed files with 67 additions and 25 deletions
+33 -23
View File
@@ -142,39 +142,49 @@ function validateHttpHeaders() {
}
/**
* Génère un token CSRF sécurisé
*
* @return string Token CSRF
* Génère un token CSRF stateless (HMAC + timestamp).
*
* Le token ne dépend pas de la session : il reste valide même si la page
* HTML est servie depuis un cache (Service Worker, CDN). Il expire après
* une durée limitée.
*
* @return string Token CSRF au format "timestamp:hash"
*/
function generateCSRFToken() {
// Démarrer la session seulement si les en-têtes n'ont pas été envoyés
if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
session_start();
}
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
$timestamp = time();
$hash = hash_hmac('sha256', (string) $timestamp, CSRF_SECRET);
return $timestamp . ':' . $hash;
}
/**
* Valide un token CSRF
*
* Valide un token CSRF stateless
*
* @param string $token Token à valider
* @return bool True si le token est valide
* @return bool True si le token est valide et non expiré
*/
function validateCSRFToken($token) {
if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
session_start();
}
if (!isset($_SESSION['csrf_token'])) {
if (empty($token) || !is_string($token)) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
$parts = explode(':', $token, 2);
if (count($parts) !== 2) {
return false;
}
[$timestamp, $hash] = $parts;
// Vérifier que le timestamp est numérique et pas trop ancien (1 heure)
if (!ctype_digit($timestamp)) {
return false;
}
$age = abs(time() - (int) $timestamp);
if ($age > 3600) {
return false;
}
$expectedHash = hash_hmac('sha256', $timestamp, CSRF_SECRET);
return hash_equals($expectedHash, $hash);
}
/**