Pendant qu'un utilisateur est connecté à votre site, une autre page malveillante peut envoyer des requêtes à votre serveur en son nom ; voilà l'essence de la question qu'est-ce que le CSRF. Le Cross-Site Request Forgery (falsification de requête intersites) est une attaque qui exploite l'habitude du navigateur d'attacher automatiquement les cookies à chaque requête. Tant que la session de l'utilisateur est active, un attaquant peut « emprunter » son autorité pour déclencher des actions qui modifient l'état, comme changer un mot de passe, transférer de l'argent ou mettre à jour des paramètres. Dans cet article, j'explique comment fonctionne l'attaque, la logique des jetons et comment les frameworks modernes automatisent cette défense, le tout avec des exemples concrets.
Comment l'attaque fonctionne, étape par étape
Supposons qu'une application bancaire effectue un virement avec un formulaire comme celui-ci :
<form action="https://banque.com/virement" method="POST">
<input name="beneficiaire" value="...">
<input name="montant" value="...">
</form>
Si l'utilisateur visite une page complètement différente tout en restant connecté à la banque, cette page peut héberger un formulaire caché :
<form action="https://banque.com/virement" method="POST" id="malveillant">
<input type="hidden" name="beneficiaire" value="attaquant">
<input type="hidden" name="montant" value="10000">
</form>
<script>document.getElementById('malveillant').submit();</script>
Lorsque le navigateur envoie la requête, il inclut automatiquement le cookie de session enregistré pour la banque. Du point de vue du serveur, la requête ressemble exactement à celle d'un utilisateur légitime. La racine du problème est la suivante : le serveur ne peut pas déterminer si la requête provient réellement de sa propre page ou d'une origine étrangère.
La logique du jeton : le jeton de synchronisation
La défense classique est le « synchronizer token pattern ». Le serveur génère une valeur aléatoire imprévisible pour chaque session, la stocke dans la session de l'utilisateur et l'intègre au formulaire de la page comme champ caché :
<form method="POST" action="/virement">
<input type="hidden" name="_token" value="a1b2c3...aleatoire">
...
</form>
Lorsque le formulaire est soumis, le serveur compare le _token reçu avec la valeur stockée dans la session. S'ils ne correspondent pas, la requête est rejetée. La page étrangère de l'attaquant ne peut pas connaître ce jeton, car une page d'une autre origine ne peut pas lire le contenu de la page bancaire à cause de la Same-Origin Policy. Le jeton reste secret et la requête falsifiée devient invalide.
Pour qu'un jeton soit sûr, il doit présenter ces propriétés :
- Imprévisible : créé avec un générateur aléatoire cryptographiquement robuste.
- Lié à la session ou à l'utilisateur : le jeton d'un utilisateur ne doit pas être valide pour un autre.
- Requis uniquement pour les requêtes modifiant l'état : les requêtes en lecture seule comme
GETne devraient de toute façon avoir aucun effet de bord.
La méthode du double submit cookie
Pour les API sans état (stateless) qui ne veulent pas conserver l'état de session sur le serveur, il existe un modèle alternatif : le double submit cookie. Ici, le jeton est écrit à la fois dans un cookie et dans un en-tête (ou le corps) de la requête. Le serveur vérifie si les deux sont égaux. Un site étranger ne peut pas lire le cookie d'une autre origine via JavaScript pour le copier dans l'en-tête, donc l'attaque échoue. Son seul inconvénient est qu'il exige une configuration soignée sur des questions comme la confiance entre sous-domaines.
Les cookies SameSite : une défense au niveau du navigateur
L'attribut SameSite ajouté aux cookies indique au navigateur si le cookie doit être envoyé lors des requêtes intersites. Il existe trois valeurs :
SameSite=Strict: le cookie n'est attaché qu'aux requêtes provenant du même site ; il n'est même pas envoyé lorsqu'on suit un lien externe.SameSite=Lax: il est attaché aux requêtesGETde navigation de premier niveau mais pas auxPOSTintersites. C'est la valeur par défaut dans la plupart des navigateurs modernes.SameSite=None; Secure: il est envoyé dans tous les cas ; à utiliser lorsque vous avez réellement besoin d'un cookie intersites.
Lax ou Strict offre une couche solide contre le CSRF, car le cookie de session n'est jamais attaché à un POST intersites déclenché par un attaquant. Pourtant, SameSite seul n'est pas considéré comme suffisant ; pour les anciens navigateurs et certains cas limites, utiliser la protection par jeton en parallèle est l'approche la plus robuste. Une défense en couches est essentielle.
Comment les frameworks automatisent cela
Les frameworks modernes gèrent la génération et la validation des jetons à votre place. Dans Laravel, par exemple, la directive Blade @csrf ajoute le champ caché du jeton au formulaire, et le middleware VerifyCsrfToken le valide automatiquement à chaque requête POST, PUT, PATCH et DELETE :
<form method="POST" action="/profil">
@csrf
<input name="nom">
<button>Enregistrer</button>
</form>
Lorsque vous lancez une requête avec JavaScript (par exemple fetch), vous devez lire le jeton depuis une balise <meta> et l'ajouter à l'en-tête :
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch('/profil', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ nom: 'Aslain' })
});
La logique est la même dans les autres écosystèmes : Django utilise {% csrf_token %} et CsrfViewMiddleware, Rails utilise protect_from_forgery et Express s'appuie sur des paquets de type csurf pour le même travail. Les API qui n'utilisent pas de sessions basées sur les cookies (par exemple celles qui transportent un en-tête Authorization: Bearer à chaque requête) sont naturellement plus résistantes au CSRF, car le navigateur n'attache pas cet en-tête automatiquement.
Liste de contrôle pratique
- Protégez chaque endpoint modifiant l'état (
POST/PUT/PATCH/DELETE) avec une validation CSRF. - Ajoutez
SameSite=Lax(ouStrictle cas échéant) etSecureaux cookies de session. - Ne laissez jamais fuiter le jeton dans une URL ou un journal ; utilisez un champ de formulaire caché ou un en-tête de requête.
- Gardez les requêtes
GETsans effet de bord ; ne les utilisez pas pour modifier des données. - Ne désactivez pas la protection intégrée du framework ; si vous le devez vraiment, n'exemptez que la route concernée.
Questions fréquentes
Le CSRF et le XSS sont-ils la même chose ?
Non. Le XSS consiste pour un attaquant à injecter un script malveillant dans votre page et peut même contourner la protection par jeton. Le CSRF consiste à abuser de la session existante de l'utilisateur depuis l'extérieur. Si une vulnérabilité XSS existe, votre défense CSRF peut s'effondrer aussi, c'est pourquoi les deux doivent être traités ensemble.
Utiliser HTTPS seul empêche-t-il le CSRF ?
Non. HTTPS chiffre les données et rend l'attaque de l'homme du milieu plus difficile, mais il ne se soucie pas de l'origine de la requête. Vous avez toujours besoin de jetons et/ou de cookies SameSite pour le CSRF.
Le jeton doit-il changer à chaque requête ?
Pas nécessairement. Un jeton fixe pour la session mais lié à celle-ci suffit dans la plupart des cas. Un jeton régénéré à chaque requête ajoute une sécurité supplémentaire mais peut poser problème avec le bouton « retour » et l'usage multi-onglets ; c'est pourquoi la plupart des frameworks utilisent un seul jeton pour toute la durée de la session.
Vous voulez sécuriser vos formulaires ou auditer une application existante ? Je conçois des solutions pratiques pour le CSRF, le XSS et la sécurité des sessions. Contactez-moi et renforçons votre projet ensemble.