Skip to Content

Chapitre 8.1 — CSRF & défenses

⏱️ TL;DR — Le CSRF (Cross-Site Request Forgery) exploite un fait vu en Partie 2 : le navigateur attache automatiquement les cookies à toute requête vers un domaine, même si la requête est déclenchée par un autre site. Un attaquant fait donc envoyer, depuis un site piégé, une requête vers ton appli ; le cookie de session de la victime authentifiée part avec, et le serveur exécute l’action en son nom (changer son email, transférer, supprimer). L’attaquant n’a pas besoin de lire la réponse (la SOP l’en empêche) — l’effet de bord suffit. Parades, en couches : jeton anti-CSRF (le nerf), SameSite sur le cookie, et vérification de l’en-tête Origin.

🎯 Objectifs

  • Comprendre le mécanisme du CSRF (cookies auto-envoyés, effet de bord sans lecture).
  • Identifier les requêtes vulnérables (celles qui changent l’état via un cookie de session).
  • Implémenter la parade principale : le jeton anti-CSRF (synchronizer token, double submit).
  • Compléter par SameSite et la vérification d’Origin (defense in depth).

Le mécanisme

Rappel clé de la Partie 2 : les cookies d’un domaine sont renvoyés automatiquement par le navigateur à chaque requête vers ce domaine — peu importe qui déclenche la requête. Un formulaire ou une balise sur evil.example peut donc provoquer une requête vers app.formacampus.fr, et le cookie de session de la victime partira avec.

Scénario type : la victime est connectée à sa banque (ou à FormaCampus). Elle visite un site piégé qui contient :

<!-- Sur evil.example : un formulaire qui se soumet tout seul vers la vraie appli --> <form action="https://app.formacampus.fr/api/email" method="POST" id="f"> <input type="hidden" name="email" value="attaquant@evil.example"> </form> <script>document.getElementById('f').submit()</script>

Au chargement, le formulaire s’envoie vers FormaCampus. Le navigateur de la victime attache son cookie de session ; côté serveur, la requête paraît légitime (session valide) et l’action s’exécute : l’email du compte devient celui de l’attaquant (qui peut ensuite déclencher un reset de mot de passe et prendre le compte).

Deux points essentiels :

  • L’attaquant ne lit jamais la réponse (la Same-Origin Policy le lui interdit — Partie 2). Il n’en a pas besoin : c’est l’effet de bord (l’action) qui l’intéresse. C’est pourquoi la SOP seule n’arrête pas le CSRF.
  • Le CSRF vise les requêtes qui changent l’état (POST/PUT/DELETE : virement, changement d’email, suppression). Une simple lecture n’intéresse pas l’attaquant (il ne verrait rien).

🎯 Côté attaquant — L’attaquant cherche les endpoints qui changent l’état et ne dépendent que du cookie de session (aucun secret imprévisible dans la requête). Il fabrique une page qui déclenche la requête (formulaire auto-submit, <img> pour un GET mal conçu, fetch), puis attire une victime authentifiée dessus (lien piégé, pub, iframe). Si l’action passe sans jeton anti-CSRF, il agit au nom de la victime. Les endpoints qui exécutent une action sensible via un simple GET sont ses préférés (déclenchables par une simple <img src>).

Parade principale : le jeton anti-CSRF

L’idée : exiger, pour toute action sensible, une valeur secrète et imprévisible que seul ton propre site peut connaître — donc qu’un site tiers ne peut pas fournir. C’est le jeton anti-CSRF (CSRF token).

Le patron classique (synchronizer token) :

  1. Le serveur génère un jeton aléatoire (CSPRNG — Partie 3), lié à la session de l’utilisateur.
  2. Il l’inclut dans les formulaires/pages (champ caché, ou exposé pour le JS).
  3. À chaque requête modifiant l’état, le client renvoie ce jeton (champ de formulaire ou en-tête).
  4. Le serveur vérifie que le jeton reçu correspond à celui de la session. Absent ou faux → rejet.
<!-- Le formulaire legitime porte le jeton (que evil.example ne peut PAS connaitre) --> <form action="/api/email" method="POST"> <input type="hidden" name="_csrf" value="jeton-aleatoire-de-la-session"> <input name="email" value="..."> </form>

Pourquoi ça marche : le site attaquant ne peut pas lire le jeton (il vit dans ta page, protégé par la SOP) ni le deviner (imprévisible). Sa requête forgée n’a pas le bon jeton → elle est rejetée. C’est le nerf de la défense CSRF.

Variante pour les API/SPA : le double submit cookie (le jeton est envoyé à la fois dans un cookie et dans un en-tête X-CSRF-Token que le JS recopie ; le serveur vérifie qu’ils correspondent — un site tiers ne peut pas lire le cookie pour le recopier dans l’en-tête). Les frameworks gèrent ça pour toi (Symfony, chapitre Partie 9).

💡 Réflexe — Utilise le mécanisme anti-CSRF de ton framework plutôt que de le réinventer : Symfony (jetons de formulaire, csrf_token()), les protections de la plupart des frameworks web. Ton job : l’activer sur toutes les actions qui changent l’état, et ne pas l’oublier sur les endpoints d’API. Ne roule pas ta propre implémentation de jeton — les subtilités (liaison à la session, comparaison en temps constant, régénération) sont déjà gérées.

Couches complémentaires : SameSite et Origin

Le jeton est la défense principale, mais on empile (defense in depth) :

  • SameSite sur le cookie de session (Partie 2) : SameSite=Lax (souvent le défaut moderne) bloque l’envoi du cookie sur les POST cross-site — ce qui neutralise déjà la forme la plus courante de CSRF, avant même la vérification du jeton. Strict va plus loin (au prix de l’UX des liens entrants). C’est une excellente première ligne, mais on ne s’en contente pas (comportements variables, cas GET, anciens navigateurs).
  • Vérifier l’en-tête Origin (et à défaut Referer) sur les requêtes modifiant l’état : le navigateur pose Origin de façon fiable, et une requête cross-site aura une Origin différente de la tienne → tu peux la rejeter. C’est une couche simple et robuste, surtout utile pour les API.
# Requete forgee depuis evil.example : Origin: https://evil.example <- differente de la tienne -> rejeter # Requete legitime depuis ton site : Origin: https://app.formacampus.fr

⚠️ Piège — Ne compte pas sur SameSite seul pour régler le CSRF, et ne te repose pas non plus sur des « défenses » qui n’en sont pas : vérifier un en-tête X-Requested-With (posable par un attaquant hors navigateur), ou se fier au type de contenu. La combinaison robuste reste : jeton anti-CSRF (principal) + SameSite + contrôle d’Origin. Et surtout : n’utilise jamais de GET pour une action qui change l’état (un GET est déclenchable par une simple <img src> et souvent hors de portée des protections) — réserve les mutations aux POST/PUT/DELETE protégés.

🧭 Sur FormaCampus — FormaCampus protège toutes les actions modifiant l’état par trois couches : le jeton anti-CSRF du framework (formulaires Symfony et en-tête X-CSRF-Token pour les appels de la SPA), le cookie de session en SameSite=Lax (qui bloque déjà les POST cross-site classiques), et une vérification d’Origin côté API (rejet si l’origine ne correspond pas à app.formacampus.fr). Règle d’équipe : aucune mutation via GET, et tout nouvel endpoint d’écriture doit passer le contrôle CSRF (vérifié en revue). Ainsi, une page piégée qui tenterait de changer l’email d’un formateur connecté échouerait sur le jeton et sur SameSite et sur Origin.

✏️ Exercices

Exercice 1 — Pourquoi la SOP n’arrête pas le CSRF. Un dev pense être protégé du CSRF « parce que la Same-Origin Policy empêche evil.example de lire les réponses de mon API ». Explique pourquoi c’est faux.

✅ Solution

La SOP empêche bien evil.example de lire la réponse cross-origin — mais le CSRF n’a pas besoin de lire : il exploite l’effet de bord. La requête forgée part quand même (la SOP n’empêche pas l’envoi), le navigateur y attache le cookie de session de la victime, et le serveur exécute l’action (changer l’email, transférer…). L’attaquant se moque de ne pas voir la réponse : l’action a eu lieu au nom de la victime. C’est pourquoi il faut une défense dédiée (jeton anti-CSRF, SameSite, Origin), la SOP étant insuffisante ici.

Exercice 2 — Le GET dangereux. Un endpoint permet de supprimer un cours via GET /cours/42/supprimer. Pourquoi est-ce particulièrement exposé au CSRF, et que faire ?

✅ Solution

Un GET qui change l’état est trivialement déclenchable sans interaction : une simple balise <img src="https://app.formacampus.fr/cours/42/supprimer"> sur un site piégé (ou dans un email affichant des images) provoque la requête, cookie de session joint → suppression au nom de la victime. Les GET sont souvent hors des protections CSRF (pas de corps, déclenchés par des ressources passives). Correctifs : (1) ne jamais faire de mutation via GET — utiliser POST/PUT/DELETE ; (2) protéger cette action par un jeton anti-CSRF ; (3) SameSite + contrôle d’Origin. Respecter la sémantique HTTP (GET = lecture sûre et idempotente) est aussi une mesure de sécurité.

🧠 Quiz de révision

1. Sur quel comportement du navigateur repose le CSRF ?

Sur l’envoi automatique des cookies : le navigateur joint les cookies d’un domaine à toute requête vers ce domaine, même déclenchée par un autre site. Le cookie de session de la victime part donc avec une requête forgée, qui paraît légitime au serveur.

2. Pourquoi la Same-Origin Policy n’empêche-t-elle pas le CSRF ?

Parce que la SOP bloque la lecture de la réponse cross-origin, mais pas l’envoi de la requête. Le CSRF ne cherche pas à lire : il veut l’effet de bord (l’action). L’action s’exécute même si l’attaquant ne voit jamais la réponse.

3. Quelle est la parade principale, et pourquoi marche-t-elle ?

Le jeton anti-CSRF : une valeur aléatoire et imprévisible, liée à la session, exigée pour toute action modifiant l’état. Un site tiers ne peut pas la lire (SOP) ni la deviner → sa requête forgée n’a pas le bon jeton et est rejetée.

4. Comment SameSite et Origin complètent-ils la défense ?

SameSite=Lax bloque l’envoi du cookie de session sur les POST cross-site (neutralise la forme courante de CSRF avant même le jeton). Vérifier l’Origin permet de rejeter les requêtes dont l’origine diffère de la tienne. Ce sont des couches supplémentaires ; le jeton reste le nerf.

5. Pourquoi ne jamais utiliser un GET pour une action qui change l’état ?

Parce qu’un GET est déclenchable par une simple ressource passive (<img src=...>), sans interaction, et souvent hors des protections CSRF. Les mutations doivent passer par POST/PUT/DELETE protégés par un jeton anti-CSRF. Respecter « GET = lecture sûre » est aussi une mesure de sécurité.


Chapitre suivant : SSRF côté serveur — la symétrie du CSRF : cette fois, c’est le serveur qu’on force à requêter pour l’attaquant, avec des conséquences cloud redoutables.

Last updated on