Un navigateur envoie vos cookies avec toute requête vers votre domaine, quel qu'en soit l'auteur. Une page d'un autre site peut donc contenir un formulaire qui poste vers le vôtre, et si votre visiteur est connecté, la requête arrive authentifiée — et paraît parfaitement légitime dans vos journaux.

Ce que cela permet

  • Changer une adresse e-mail, puis demander une réinitialisation du mot de passe vers la nouvelle.
  • Passer une commande, transférer quelque chose, supprimer quelque chose.
  • Tout ce que votre site fait avec un POST et une session.

Le jeton

Une valeur aléatoire conservée en session et répétée dans le formulaire. Une page d'un autre site ne peut pas lire votre session, donc ne peut pas connaître la valeur.

// when rendering the form
$_SESSION["csrf"] = $_SESSION["csrf"] ?? bin2hex(random_bytes(32));
?>
<input type="hidden" name="csrf" value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
// when handling it
if (!hash_equals($_SESSION["csrf"] ?? "", $_POST["csrf"] ?? "")) {
    http_response_code(400); exit;
}
Comparez avec hash_equals, pas avec ==. Une comparaison ordinaire s'arrête au premier caractère différent, et le temps qu'elle prend révèle quelle part du jeton était juste.

Et l'attribut du cookie

session.cookie_samesite = Lax

SameSite=Lax dit au navigateur de ne pas envoyer le cookie du tout lors d'un POST inter-sites, ce qui arrête cette classe d'attaque avant que votre code ne la voie. C'est une seconde couche, pas un remplacement : elle ne protège que là où le navigateur est récent et se conduit bien.

Un GET ne doit rien modifier

Un lien qui supprime quelque chose est un CSRF en attente — et pire, un robot ou un aperçu de lien le déclenchera tout seul. Tout ce qui change un état passe par un POST avec jeton.
Un framework a cela intégré et activé. Si vous l'avez désactivé sur un point d'entrée pour faire marcher quelque chose, c'est précisément celui-là qu'il faut regarder.