Suivi / résolution : "Token CSRF POST invalide"
Après analyse du code, voici ce que j'ai trouvé sur l'origine du problème, ainsi que les correctifs que j'ai mis en place (sur une instance de test, en cours de validation avant la prod). Je partage au cas où ça aide d'autres utilisateurs, et aussi pour faire remonter le point à l'équipe GestSup.
Ce qui se passe
Le jeton anti-CSRF est généré une seule fois par session :
Code : Tout sélectionner
index.php → if(!isset($_SESSION['csrf_post'])) { $_SESSION['csrf_post'] = bin2hex(random_bytes(32)); }
Il est ensuite figé dans le formulaire du ticket au moment de l'affichage (champ caché "csrf_post") et n'est jamais rafraîchi côté navigateur.
Du coup, si la session PHP se termine pendant qu'un ticket reste ouvert (déconnexion automatique via le paramètre "Temps de déconnexion", ou expiration des fichiers de session côté PHP via session.gc_maxlifetime), la requête suivante repart sur une nouvelle session : un nouveau jeton est généré côté serveur, mais le formulaire renvoie encore l'ancien. La comparaison ne correspond plus, dans core/ticket.php :
Code : Tout sélectionner
if(... $_POST['csrf_post'] != $_SESSION['csrf_post']) { die(DisplayMessage('error', T_('Token CSRF POST invalide'))); }
Et le die() arrête la page, ce qui fait perdre toute la saisie. Ça correspond bien aux symptômes décrits : le message apparaît après quelques minutes, ou quand on laisse un ticket en attente et qu'on y revient plus tard.
Détail repéré au passage
Dans cette même vérification, strtoupper($_SERVER['REQUEST_METHOD']=='POST') applique strtoupper au résultat du test plutôt qu'à la méthode HTTP. Sans effet concret sur les requêtes POST, mais ce serait plus juste écrit strtoupper($_SERVER['REQUEST_METHOD'])=='POST' (on le retrouve aussi dans admin/users/add.php et edit.php).
Ce que j'ai fait de mon côté
1) Allonger la durée de vie des sessions pour qu'elles ne se terminent plus en pleine saisie :
- php.ini (pool FPM) : session.gc_maxlifetime = 46800 (13 h)
- GestSup > Administration > Paramètres > Général > "Temps de déconnexion" = 720 (12 h)
- Le "Temps de déconnexion" doit rester inférieur à session.gc_maxlifetime.
2) Remplacer le die() par une gestion plus souple : quand le jeton est périmé, on régénère un jeton et on réaffiche le formulaire avec la saisie conservée + un message d'information, sans enregistrer la requête refusée. La protection reste donc en place, mais l'utilisateur ne perd plus son travail.
Piste pour une version officielle
Si l'équipe GestSup le juge utile, ne plus perdre la saisie quand le jeton a expiré (et/ou rafraîchir le jeton côté navigateur) réglerait le souci à la source. Ça semble concerner plusieurs personnes sur ce fil. Merci pour le travail réalisé sur GestSup !