fix(csrf): stateless HMAC token to survive page cache
This commit is contained in:
+22
-2
@@ -695,6 +695,16 @@ TIP: Si vous déployez une instance indépendante, remplacez `LIBERAPAY_URL` par
|
|||||||
|
|
||||||
NOTE: Le cache de l'API PeerTube et de WordPress (`includes/simple-cache.php`) est *toujours actif*, indépendamment de `CACHE_ENABLED`, avec des TTL par endpoint (catégories 1 h, vidéos/recherche 10 min, articles WP 15 min, comptes 5 min). Les fichiers sont stockés dans `cache/api/`.
|
NOTE: Le cache de l'API PeerTube et de WordPress (`includes/simple-cache.php`) est *toujours actif*, indépendamment de `CACHE_ENABLED`, avec des TTL par endpoint (catégories 1 h, vidéos/recherche 10 min, articles WP 15 min, comptes 5 min). Les fichiers sont stockés dans `cache/api/`.
|
||||||
|
|
||||||
|
.Sécurité
|
||||||
|
[cols="2,2,3",options="header"]
|
||||||
|
|===
|
||||||
|
| Constante | Défaut | Description
|
||||||
|
|
||||||
|
| `CSRF_SECRET`
|
||||||
|
| `'change-me-in-config-local-php'`
|
||||||
|
| Clé secrète utilisée pour signer les tokens CSRF stateless. À remplacer impérativement dans `config.local.php` par une valeur aléatoire (`php -r "echo bin2hex(random_bytes(32)) . PHP_EOL;"`)
|
||||||
|
|===
|
||||||
|
|
||||||
.Bloc « À propos » (pas de valeur par défaut — bloc masqué tant que non défini)
|
.Bloc « À propos » (pas de valeur par défaut — bloc masqué tant que non défini)
|
||||||
[cols="2,3",options="header"]
|
[cols="2,3",options="header"]
|
||||||
|===
|
|===
|
||||||
@@ -784,7 +794,7 @@ Mise en page desktop : hero/Castopod/Funkwhale + Mastodon + WordPress s'organise
|
|||||||
- *En-têtes* : `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`, `X-XSS-Protection`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy` restrictive, HSTS (HTTPS uniquement), CORP/COOP `same-origin`
|
- *En-têtes* : `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`, `X-XSS-Protection`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy` restrictive, HSTS (HTTPS uniquement), CORP/COOP `same-origin`
|
||||||
+
|
+
|
||||||
NOTE: L'en-tête COEP a été *volontairement retiré* (commit `62d4d99`) : `require-corp` bloquait les embeds cross-origin de PeerTube, Mastodon, Castopod et Funkwhale.
|
NOTE: L'en-tête COEP a été *volontairement retiré* (commit `62d4d99`) : `require-corp` bloquait les embeds cross-origin de PeerTube, Mastodon, Castopod et Funkwhale.
|
||||||
- *CSRF* : jeton de session (`random_bytes(32)`) injecté en `<meta>`, vérifié par `hash_equals` sur l'endpoint AJAX ; vérification de l'en-tête `Origin`
|
- *CSRF* : jeton stateless HMAC-SHA256 (timestamp + signature, validité 1 h) injecté en `<meta>`, vérifié par `hash_equals` sur l'endpoint AJAX ; vérification de l'en-tête `Origin` ou du `Referer`
|
||||||
- *Anti-SSRF* : validation des URLs d'instances (schéma http/https, blocage IP privées et `localhost`), liste blanche des endpoints de l'API PeerTube, cURL sans redirections
|
- *Anti-SSRF* : validation des URLs d'instances (schéma http/https, blocage IP privées et `localhost`), liste blanche des endpoints de l'API PeerTube, cURL sans redirections
|
||||||
- *Anti-XSS* : échappement systématique des sorties ; validation des entrées (UUID vidéo, requête de recherche ≤ 200 car., numéros de page, ID de catégorie 1–20)
|
- *Anti-XSS* : échappement systématique des sorties ; validation des entrées (UUID vidéo, requête de recherche ≤ 200 car., numéros de page, ID de catégorie 1–20)
|
||||||
|
|
||||||
@@ -1632,6 +1642,16 @@ TIP: If you deploy an independent instance, replace `LIBERAPAY_URL` with your ow
|
|||||||
|
|
||||||
NOTE: The PeerTube API and WordPress cache (`includes/simple-cache.php`) is *always on*, regardless of `CACHE_ENABLED`, with per-endpoint TTLs (categories 1 h, videos/search 10 min, WP posts 15 min, accounts 5 min). Files are stored in `cache/api/`.
|
NOTE: The PeerTube API and WordPress cache (`includes/simple-cache.php`) is *always on*, regardless of `CACHE_ENABLED`, with per-endpoint TTLs (categories 1 h, videos/search 10 min, WP posts 15 min, accounts 5 min). Files are stored in `cache/api/`.
|
||||||
|
|
||||||
|
.Security
|
||||||
|
[cols="2,2,3",options="header"]
|
||||||
|
|===
|
||||||
|
| Constant | Default | Description
|
||||||
|
|
||||||
|
| `CSRF_SECRET`
|
||||||
|
| `'change-me-in-config-local-php'`
|
||||||
|
| Secret key used to sign stateless CSRF tokens. Must be replaced in `config.local.php` with a random value (`php -r "echo bin2hex(random_bytes(32)) . PHP_EOL;"`)
|
||||||
|
|===
|
||||||
|
|
||||||
."About" block (no default values — block hidden until defined)
|
."About" block (no default values — block hidden until defined)
|
||||||
[cols="2,3",options="header"]
|
[cols="2,3",options="header"]
|
||||||
|===
|
|===
|
||||||
@@ -1721,7 +1741,7 @@ Desktop layout: hero/Castopod/Funkwhale + Mastodon + WordPress are arranged in c
|
|||||||
- *Headers*: `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`, `X-XSS-Protection`, `Referrer-Policy: strict-origin-when-cross-origin`, restrictive `Permissions-Policy`, HSTS (HTTPS only), CORP/COOP `same-origin`
|
- *Headers*: `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`, `X-XSS-Protection`, `Referrer-Policy: strict-origin-when-cross-origin`, restrictive `Permissions-Policy`, HSTS (HTTPS only), CORP/COOP `same-origin`
|
||||||
+
|
+
|
||||||
NOTE: The COEP header was *deliberately removed* (commit `62d4d99`): `require-corp` broke cross-origin embeds from PeerTube, Mastodon, Castopod and Funkwhale.
|
NOTE: The COEP header was *deliberately removed* (commit `62d4d99`): `require-corp` broke cross-origin embeds from PeerTube, Mastodon, Castopod and Funkwhale.
|
||||||
- *CSRF*: session token (`random_bytes(32)`) injected as a `<meta>` tag, verified with `hash_equals` on the AJAX endpoint; `Origin` header verification
|
- *CSRF*: stateless HMAC-SHA256 token (timestamp + signature, 1 h validity) injected as a `<meta>` tag, verified with `hash_equals` on the AJAX endpoint; `Origin` or `Referer` header verification
|
||||||
- *Anti-SSRF*: instance URL validation (http/https scheme, blocking private IPs and `localhost`), allowlist of PeerTube API endpoints, cURL without redirects
|
- *Anti-SSRF*: instance URL validation (http/https scheme, blocking private IPs and `localhost`), allowlist of PeerTube API endpoints, cURL without redirects
|
||||||
- *Anti-XSS*: systematic output escaping; input validation (video UUID, search query ≤ 200 chars, page numbers, category ID 1–20)
|
- *Anti-XSS*: systematic output escaping; input validation (video UUID, search query ≤ 200 chars, page numbers, category ID 1–20)
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,13 @@ if (!defined('ENABLE_USER_ACCOUNTS')) define('ENABLE_USER_ACCOUNTS', false);
|
|||||||
if (!defined('CACHE_ENABLED')) define('CACHE_ENABLED', true);
|
if (!defined('CACHE_ENABLED')) define('CACHE_ENABLED', true);
|
||||||
if (!defined('CACHE_DURATION')) define('CACHE_DURATION', 3600); // En secondes (1 heure)
|
if (!defined('CACHE_DURATION')) define('CACHE_DURATION', 3600); // En secondes (1 heure)
|
||||||
|
|
||||||
|
// Clé secrète utilisée pour signer les tokens CSRF stateless.
|
||||||
|
// À remplacer impérativement dans config.local.php par une valeur aléatoire
|
||||||
|
// propre à l'instance (ex. bin2hex(random_bytes(32))).
|
||||||
|
if (!defined('CSRF_SECRET')) {
|
||||||
|
define('CSRF_SECRET', 'change-me-in-config-local-php');
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// Configuration de la section Hero (bannière d'accueil)
|
// Configuration de la section Hero (bannière d'accueil)
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|||||||
@@ -331,6 +331,11 @@ define('WORDPRESS_ENABLED', false);
|
|||||||
// define('CACHE_ENABLED', true);
|
// define('CACHE_ENABLED', true);
|
||||||
// define('CACHE_DURATION', 3600); // 1 heure recommandé
|
// define('CACHE_DURATION', 3600); // 1 heure recommandé
|
||||||
|
|
||||||
|
// Clé secrète utilisée pour signer les tokens CSRF stateless.
|
||||||
|
// Générez une valeur aléatoire unique pour votre instance :
|
||||||
|
// php -r "echo bin2hex(random_bytes(32)) . PHP_EOL;"
|
||||||
|
// define('CSRF_SECRET', 'votre-cle-secrete-aleatoire');
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// Intégration Funkwhale (Musique)
|
// Intégration Funkwhale (Musique)
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|||||||
+33
-23
@@ -142,39 +142,49 @@ function validateHttpHeaders() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Génère un token CSRF sécurisé
|
* Génère un token CSRF stateless (HMAC + timestamp).
|
||||||
*
|
*
|
||||||
* @return string Token CSRF
|
* 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() {
|
function generateCSRFToken() {
|
||||||
// Démarrer la session seulement si les en-têtes n'ont pas été envoyés
|
$timestamp = time();
|
||||||
if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
|
$hash = hash_hmac('sha256', (string) $timestamp, CSRF_SECRET);
|
||||||
session_start();
|
return $timestamp . ':' . $hash;
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($_SESSION['csrf_token'])) {
|
|
||||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $_SESSION['csrf_token'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valide un token CSRF
|
* Valide un token CSRF stateless
|
||||||
*
|
*
|
||||||
* @param string $token Token à valider
|
* @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) {
|
function validateCSRFToken($token) {
|
||||||
if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
|
if (empty($token) || !is_string($token)) {
|
||||||
session_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($_SESSION['csrf_token'])) {
|
|
||||||
return false;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user