Leçon 9/14 12 min

CSRF (Cross-Site Request Forgery)

Une page piégée déclenche un virement car le navigateur joint le cookie. Vide un compte, puis la défense : token CSRF.

Le virement que vous n'avez pas demandé

Vous êtes connecté à votre banque dans un onglet (donc votre cookie de session est posé, leçon 8). Un ami vous envoie un lien rigolo, vous l'ouvrez dans un autre onglet. La page a l'air d'un simple mème. Mais elle contient ça, invisible :

Petite précision avant de voir le code : le SameSite=Lax posé à la leçon 8 atténue le problème, mais ne le règle pas entièrement. Ce qui fait vraiment la force du CSRF, c'est que le navigateur joint le cookie du domaine à toute requête qu'il lui envoie, même déclenchée depuis un autre onglet, indépendamment de l'onglet d'origine.

<!-- sur evil.com, caché -->
<form action="https://banque.com/virement" method="POST" id="f">
  <input type="hidden" name="vers" value="pirate">
  <input type="hidden" name="montant" value="1000">
</form>
<script>document.getElementById('f').submit();</script>

Le formulaire s'envoie tout seul vers banque.com. Et là, le détail qui fait tout : votre navigateur voit une requête vers banque.com, alors il y joint automatiquement votre cookie de session de la banque. La banque reçoit donc un ordre de virement parfaitement authentifié, le vôtre, et l'exécute. Vous n'avez fait qu'ouvrir une page.

Ça, c'est le CSRF (Cross-Site Request Forgery) : faire exécuter une action par le navigateur de la victime, sur un site où elle est connectée, à son insu. C'est lié à l'OWASP 2025 (Broken Access Control).

Pourquoi ça marche

La cause tient à une habitude du navigateur : il joint le cookie d'un domaine à chaque requête vers ce domaine, même quand la requête part d'un autre site. Le serveur reçoit donc le cookie, voit une session valide, et fait confiance. Il ne peut pas faire la différence entre une vraie requête (venue de son propre formulaire) et une requête forgée (venue d'evil.com) : les deux portent le même cookie.

Point important : l'attaquant n'a même pas besoin de lire la réponse. La same-origin policy l'en empêche, mais peu importe : il lui suffit que l'action ait lieu. Le CSRF vise donc tout ce qui change un état : un virement, un changement d'email ou de mot de passe, une suppression de compte.

Le déroulé d'une attaque CSRF en quatre étapes 1 Vous êtes connecté à banque.com (cookie posé) 2 Vous ouvrez evil.com : un formulaire caché s'arme 3 Sa requête file vers banque.com, cookie joint tout seul 4 banque.com voit un cookie valide : virement exécuté
Le cookie prouve qui vous êtes, jamais que c'est vous qui avez voulu la requête. Le serveur ne fait confiance qu'au cookie.

CSRF n'est pas XSS. Le XSS injecte du code dans le site (il s'exécute au nom du site et peut tout lire). Le CSRF tire une requête à l'aveugle depuis un autre site (il ne lit rien, il ne fait qu'agir). Le CSRF n'exige aucune faille sur la page visée : il abuse simplement l'envoi automatique des cookies par le navigateur.

Le faux remède : « je passe l'action en POST au lieu de GET » est nécessaire (un GET se déclenche avec une simple balise <img>), mais pas suffisant : un formulaire caché qui s'auto-envoie fait un POST tout aussi facilement. Et « je vérifie l'en-tête Referer » est fragile (il est souvent absent ou retiré). La vraie parade est le token CSRF (section 4).

À vous d'attaquer : videz le compte

Vous êtes la victime, connectée à banque.com (solde 5 000 €). Vous, l'attaquant, avez aussi préparé une page piégée sur evil.com. Cliquez pour « visiter » cette page : elle déclenche un virement vers votre compte de pirate, sans que la victime ne touche à rien. Essayez d'abord sans protection, puis activez le token CSRF et recommencez. Tout est simulé dans votre navigateur.

banque.com · connecté en tant que vous (la victime)
evil.com · page piégée (un mème, en apparence)

        
Ce qui vient de se passer

Sans protection, la page evil.com envoie un POST vers banque.com/virement. Le navigateur y joint votre cookie de session, la banque y voit une demande légitime et exécute le virement. Le solde fond, sans aucune action de la victime.

Avec le token CSRF, la banque exige en plus un jeton secret, présent seulement dans ses propres formulaires. La page evil.com ne peut pas le connaître (la same-origin policy l'empêche de lire la page de la banque), donc sa requête arrive sans jeton valide et la banque la refuse (403). Le solde ne bouge pas.

Le correctif : le token CSRF

La défense de référence est le token CSRF (ou « jeton synchroniseur »). L'idée est simple : à chaque formulaire, le serveur glisse un jeton secret et aléatoire, lié à la session. À la réception, il vérifie que ce jeton est bien présent et correct. La page de l'attaquant, elle, ne peut pas connaître ce jeton : la same-origin policy l'empêche de lire la page de la banque. Sa requête forgée arrive donc sans jeton valide, et elle est rejetée.

En PHP, on génère le jeton à l'affichage du formulaire, et on le vérifie à la réception :

// à l'affichage : un jeton aléatoire, stocké dans la session
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
// dans le formulaire :
// <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">

// à la réception : on compare (en temps constant)
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
    http_response_code(403);
    exit; // jeton absent ou faux : on refuse
}

Deuxième couche, vue à la leçon 8 : l'attribut SameSite=Lax sur le cookie de session. Il empêche le cookie de partir sur un POST venu d'un autre site. Résultat : la plupart des CSRF sont coupés à la source. C'est la valeur par défaut des navigateurs modernes, mais on garde quand même le token : ensemble, ils se renforcent.

Le cas des API. Pour une API en JSON, il y a une protection naturelle : une page d'attaque ne peut pas ajouter d'en-tête personnalisé sur une requête cross-site, le navigateur le lui interdit. Exiger un en-tête maison (par exemple X-CSRF-Token) suffit donc souvent. Et dans tous les cas : une action qui change l'état ne doit jamais passer par un simple GET, sinon une image suffit à la déclencher.

Défense en profondeur :

  1. un token CSRF sur toute action qui change un état (vérifié en temps constant) ;
  2. cookie de session en SameSite=Lax (ou Strict) ;
  3. jamais d'action sensible en GET : réservez le GET à la lecture ;
  4. pour les API : exiger un en-tête personnalisé, ou vérifier l'en-tête Origin ;
  5. re-demander le mot de passe pour les actions critiques (virement, changement d'email).

Référence : OWASP CSRF Prevention Cheat Sheet.

La méthode et l'arsenal du pentester

On connaît le mécanisme et les défenses. Voyons maintenant comment un pentester confirme qu'une protection est absente ou mal configurée.

1. Repérer les actions sensibles. Tout ce qui change un état : virement, changement d'email ou de mot de passe, ajout d'un administrateur, suppression. Pour chacune, on regarde s'il y a un jeton anti-CSRF dans le formulaire.

2. Tester l'absence de protection. On capture une requête légitime, on retire le jeton (ou on le change), et on la rejoue. Si la banque l'accepte quand même, l'action est vulnérable.

3. Construire la preuve. On écrit une petite page HTML avec un formulaire qui s'auto-envoie (ou une simple <img> pour une action en GET). On l'héberge, on amène la victime dessus, et l'action part en son nom.

L'arsenal.

  • Burp Suite : le bouton « Generate CSRF PoC » (clic droit sur une requête) fabrique la page d'attaque HTML toute prête. L'outil CSRF par excellence.
  • OWASP ZAP : détecte automatiquement les formulaires sans jeton anti-CSRF.
  • Le navigateur : héberger la page piégée et regarder l'action partir, cookie joint, dans l'onglet réseau.

L'impact, et pourquoi le CSRF a reculé

Tout ce que la victime a le droit de faire, l'attaquant le fait à sa place : un virement, un changement d'email qui mène à la prise du compte (email changé, puis « mot de passe oublié »), un achat, un réglage modifié. Et c'est silencieux : la victime ne voit rien, elle a juste ouvert une page.

Le plus frappant : ça marche même avec un mot de passe parfait et le MFA activé. Le CSRF n'attaque pas la connexion ; il abuse une session déjà ouverte. L'authentification a déjà eu lieu, l'attaquant n'a qu'à profiter du cookie.

Bonne nouvelle : le CSRF a beaucoup reculé. Depuis quelques années, les navigateurs mettent SameSite=Lax par défaut sur les cookies, ce qui bloque une grande partie des attaques automatiquement. Mais ce n'est pas zéro : vieux navigateurs, actions en GET, mauvaise configuration, API cross-origin. Le token CSRF reste la défense qu'on n'enlève pas.

Ce que ça révèle côté défense : on ne fait pas confiance au seul cookie pour prouver qu'une requête est voulue par l'utilisateur. On empile : token CSRF (l'attaquant ne peut pas le deviner), SameSite (le cookie ne part pas en cross-site), GET en lecture seule, et re-authentification pour le critique. Une seule couche ne suffit jamais (leçon 2).

La checklist CSRF

À vérifier sur chaque action qui modifie quelque chose.

  • Token CSRF sur tous les formulaires qui changent un état, vérifié en temps constant (hash_equals).
  • Cookie de session en SameSite=Lax (ou Strict).
  • GET en lecture seule : aucune action sensible derrière un GET.
  • API : exiger un en-tête personnalisé, ou vérifier Origin.
  • Actions critiques : re-demander le mot de passe.

Bonne nouvelle pour la pratique. Les frameworks gèrent le token CSRF pour vous : @csrf dans Laravel, le composant Form de Symfony, le middleware de Django, Rails… La règle est simple : ne le désactivez pas. Référence : OWASP CSRF Prevention Cheat Sheet. Pour s'entraîner : les labs CSRF de la Web Security Academy.

Rappel. Déclencher une action sur le compte de quelqu'un d'autre, même « pour voir », est une attaque réelle. On ne teste que ses propres systèmes ou une cible explicitement autorisée (leçon 1).

The transfer you never asked for

You're logged into your bank in one tab (so your session cookie is set, lesson 8). A friend sends you a funny link, you open it in another tab. The page looks like a simple meme. But it contains this, invisible:

One clarification before the code: the SameSite=Lax flag set in lesson 8 reduces the risk, but doesn't eliminate it entirely. The real power of CSRF lies in a basic browser behavior — it attaches the domain's cookie to every request sent to it, even one triggered from another tab, regardless of where the request originated.

<!-- on evil.com, hidden -->
<form action="https://bank.com/transfer" method="POST" id="f">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="1000">
</form>
<script>document.getElementById('f').submit();</script>

The form submits itself to bank.com. And here's the detail that does it all: your browser sees a request to bank.com, so it automatically attaches your bank session cookie. So the bank receives a perfectly authenticated transfer order, yours, and executes it. All you did was open a page.

That's CSRF (Cross-Site Request Forgery): making the victim's browser perform an action, on a site where they're logged in, without their knowledge. It maps to OWASP 2025 (Broken Access Control).

Why it works

The cause is a browser habit: it attaches a domain's cookie to every request to that domain, even when the request comes from another site. So the server receives the cookie, sees a valid session, and trusts it. It can't tell the difference between a real request (from its own form) and a forged one (from evil.com): both carry the same cookie.

Important point: the attacker doesn't even need to read the response. The same-origin policy stops them, but it doesn't matter: they just need the action to happen. So CSRF targets anything that changes a state: a transfer, an email or password change, an account deletion.

How a CSRF attack unfolds in four steps 1 You're logged in to bank.com (cookie set) 2 You open evil.com: a hidden form arms itself 3 Its request flies to bank.com, cookie attached on its own 4 bank.com sees a valid cookie: transfer executed
The cookie proves who you are, never that you wanted the request. The server trusts the cookie alone.

CSRF is not XSS. XSS injects code into the site (it runs as the site and can read everything). CSRF fires a request blindly from another site (it reads nothing, it only acts). CSRF needs no flaw on the target page: it simply abuses the browser's automatic cookie sending.

Beware the false cure: "I'll switch the action to POST instead of GET" is necessary (a GET fires with a simple <img> tag), but not sufficient: a hidden self-submitting form does a POST just as easily. And "I'll check the Referer header" is fragile (it's often missing or stripped). The real fix is the CSRF token (section 4).

Your turn to attack: empty the account

You're the victim, logged into bank.com (balance €5,000). You, the attacker, also prepared a booby-trapped page on evil.com. Click to "visit" that page: it triggers a transfer to your attacker account, without the victim touching anything. Try first without protection, then turn on the CSRF token and try again. Everything is simulated in your browser.

bank.com · logged in as you (the victim)
evil.com · booby-trapped page (a meme, apparently)

        
What just happened

Without protection, the evil.com page sends a POST to bank.com/transfer. The browser attaches your session cookie, the bank sees a legitimate request and executes the transfer. The balance melts, with no action from the victim.

With the CSRF token, the bank also requires a secret token, present only in its own forms. The evil.com page can't know it (the same-origin policy stops it from reading the bank's page), so its request arrives with no valid token and the bank rejects it (403). The balance doesn't move.

The fix: the CSRF token

The reference defense is the CSRF token (or "synchronizer token"). The idea is simple: in each form, the server slips a secret, random token, tied to the session. On reception, it checks the token is present and correct. The attacker's page can not know that token: the same-origin policy stops it from reading the bank's page. So its forged request arrives with no valid token, and it's rejected.

In PHP, you generate the token when showing the form, and verify it on reception:

// when showing the form: a random token, stored in the session
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
// in the form:
// <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">

// on reception: compare (in constant time)
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
    http_response_code(403);
    exit; // token missing or wrong: reject
}

Second layer, seen in lesson 8: the SameSite=Lax attribute on the session cookie. It stops the cookie from going out on a POST coming from another site. As a result, most CSRF are cut at the source. It's the default in modern browsers, but you keep the token anyway: together, they reinforce each other.

The case of APIs. For a JSON API, there's a natural protection: an attack page can't add a custom header on a cross-site request, the browser forbids it. So requiring a custom header (for example X-CSRF-Token) is often enough. And in every case: an action that changes a state must never go through a plain GET, otherwise an image is enough to trigger it.

Defense in depth:

  1. a CSRF token on every state-changing action (verified in constant time);
  2. session cookie with SameSite=Lax (or Strict);
  3. never a sensitive action over GET: keep GET for reading;
  4. for APIs: require a custom header, or check the Origin header;
  5. re-ask the password for critical actions (transfer, email change).

Reference: OWASP CSRF Prevention Cheat Sheet.

The pentester's method and arsenal

Now that we have the mechanism and the defenses, let's see how a pentester confirms that a protection is missing or misconfigured.

1. Spot the sensitive actions. Anything that changes a state: transfer, email or password change, adding an admin, deletion. For each, look for an anti-CSRF token in the form.

2. Test for missing protection. Capture a legitimate request, remove the token (or change it), and replay it. If the bank accepts it anyway, the action is vulnerable.

3. Build the proof. Write a small HTML page with a self-submitting form (or a plain <img> for a GET action). Host it, lure the victim onto it, and the action fires in their name.

The arsenal.

  • Burp Suite: the "Generate CSRF PoC" button (right-click on a request) builds the ready-made HTML attack page. The CSRF tool par excellence.
  • OWASP ZAP: automatically detects forms without an anti-CSRF token.
  • The browser: host the booby-trapped page and watch the action fire, cookie attached, in the network tab.

The impact, and why CSRF has receded

Anything the victim is allowed to do, the attacker does in their place: a transfer, an email change that leads to account takeover (email changed, then "forgot password"), a purchase, a changed setting. And it's silent: the victim sees nothing, they just opened a page.

The most striking part: it works even with a perfect password and MFA enabled. CSRF doesn't attack the login; it abuses an already-open session. Authentication already happened, the attacker just rides the cookie.

Good news: CSRF has receded a lot. For a few years now, browsers set SameSite=Lax by default on cookies, which blocks a big share of attacks automatically. But it's not zero: old browsers, GET actions, misconfiguration, cross-origin APIs. The CSRF token stays the defense you don't remove.

What this reveals for defense: you don't trust the cookie alone to prove a request is intended by the user. You stack: CSRF token (the attacker can't guess it), SameSite (the cookie doesn't go cross-site), read-only GET, and re-authentication for the critical. One layer is never enough (lesson 2).

The CSRF checklist

To check on every action that changes something.

  • CSRF token on every state-changing form, verified in constant time (hash_equals).
  • Session cookie with SameSite=Lax (or Strict).
  • Read-only GET: no sensitive action behind a GET.
  • API: require a custom header, or check Origin.
  • Critical actions: re-ask the password.

Good news for practice. Frameworks handle the CSRF token for you: @csrf in Laravel, Symfony's Form component, Django's middleware, Rails… The rule is simple: don't disable it. Reference: OWASP CSRF Prevention Cheat Sheet. To practice: the CSRF labs of the Web Security Academy.

Reminder. Triggering an action on someone else's account, even "just to see", is a real attack. Only test your own systems or an explicitly authorized target (lesson 1).

Prédisez avant de lire

Deux versions de l'endpoint de virement. La version A vérifie seulement le cookie de session. La version B vérifie aussi un token CSRF caché dans le formulaire. Avant de dérouler : laquelle une page evil.com peut-elle déclencher avec un formulaire caché qui s'auto-envoie ?

Voir la réponse

La version A. Le formulaire d'evil.com s'auto-envoie, le navigateur joint le cookie de session de la banque, et comme la banque ne vérifie que ce cookie, le virement passe. La version B résiste : elle exige aussi le token CSRF, qu'evil.com ne peut pas connaître (la same-origin policy l'empêche de lire la page de la banque). Sans token valide, la requête est refusée. Le cookie prouve qui vous êtes, le token prouve que la requête vient bien du site lui-même.

Predict before reading on

Two versions of the transfer endpoint. Version A checks only the session cookie. Version B also checks a CSRF token hidden in the form. Before you expand: which one can an evil.com page trigger with a hidden self-submitting form?

Show the answer

Version A. The evil.com form self-submits, the browser attaches the bank's session cookie, and since the bank only checks that cookie, the transfer goes through. Version B resists: it also requires the CSRF token, which evil.com can't know (the same-origin policy stops it from reading the bank's page). With no valid token, the request is rejected. The cookie proves who you are, the token proves the request really came from the site itself.

🎯 Pratique

S'entraîner (clique pour ouvrir) :

💬 Ré-explique sans regarder
Ré-explique sans regarder

Avec tes mots : pourquoi le cookie de session ne suffit pas à prouver qu'une requête est voulue par l'utilisateur, et pourquoi un token CSRF bloque l'attaque alors que le cookie, lui, part tout seul ?

Une bonne explication dit : le navigateur joint le cookie de session à toute requête vers le domaine, même quand elle part d'un autre site. Le cookie prouve donc qui vous êtes, mais pas que vous avez voulu cette requête : une page piégée peut la déclencher, cookie joint automatiquement. Le serveur, lui, ne voit que le cookie et fait confiance. Le token CSRF ajoute la preuve manquante : un secret aléatoire que le serveur place dans ses propres formulaires. Une page d'attaque ne peut pas le connaître, car la same-origin policy l'empêche de lire la page du site visé. Sa requête forgée arrive sans token valide, et le serveur la refuse. En clair : le cookie prouve l'identité, le token prouve l'intention.
🧠 Rappel libre
Rappel libre

Sans remonter : explique en quoi le CSRF diffère du XSS, pourquoi passer une action en POST ne suffit pas, et comment le token CSRF bloque l'attaque.

CSRF vs XSS : le XSS injecte du code dans le site et s'exécute en son nom (il peut tout lire) ; le CSRF tire une requête à l'aveugle depuis un autre site et ne fait qu'agir (il ne lit pas la réponse). Le POST ne suffit pas : un formulaire caché qui s'auto-envoie fait un POST aussi facilement qu'une image fait un GET, cookie joint dans les deux cas. Le token CSRF bloque l'attaque : le serveur exige un secret aléatoire placé dans ses propres formulaires. La page de l'attaquant ne peut pas le lire (same-origin policy), sa requête arrive sans token valide, et elle est refusée.
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

Tu demandes à l'IA de protéger le virement contre le CSRF. Elle répond : « Je passe l'endpoint en POST au lieu de GET. Comme ça, une simple image <img> ne peut plus déclencher le virement : c'est protégé contre le CSRF. » Tu acceptes, ou tu rejettes ?

À rejeter. Passer en POST est nécessaire (oui, ça bloque le déclenchement par une <img>, et une action qui change l'état ne doit jamais être en GET), mais c'est loin de suffire. Un attaquant met un formulaire caché qui s'auto-envoie en POST : <form method=POST>…<script>form.submit()</script>. Le navigateur joint le cookie comme avant, et le virement passe. Le POST ne change rien au cœur du problème : le serveur fait toujours confiance au seul cookie. La vraie défense est le token CSRF (un secret que la page de l'attaquant ne peut pas lire), complété par SameSite. Le POST fait partie de l'hygiène, il ne remplace pas le token.
Une page evil.com envoie un POST vers banque.com/virement et le virement passe, sans que la victime n'ait rien fait. Pourquoi ?
Quelle est la défense de référence contre le CSRF ?
Quelle est la différence clé entre CSRF et XSS ?
Un dev passe le virement de GET à POST et pense le CSRF réglé. Pourquoi a-t-il tort ?
Prochaine étape

Le CSRF abuse la confiance entre sites. La leçon 10 attaque la même frontière par l'autre bout : CORS et la Same-Origin Policy, ce que ça protège vraiment, et pourquoi ce n'est PAS une sécurité serveur.

Leçon 10 : CORS et Same-Origin Policy →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement