Module 2Programmer avec Laravel
Chapitre 5 sur 17
Protection CSRF
L'attaque CSRF, le jeton @csrf, l'erreur 419, les exclusions et les en-têtes X-CSRF-TOKEN et X-XSRF-TOKEN.
- ≈ 160 min
- 6 leçons
- 1 vidéo
- 4 exercices
Module 2Programmer avec Laravel
Chapitre 5 sur 17
L'attaque CSRF, le jeton @csrf, l'erreur 419, les exclusions et les en-têtes X-CSRF-TOKEN et X-XSRF-TOKEN.
Objectifs
Avec ce chapitre, tu sauras…
@csrf et comprendre l'erreur 419 ;X-CSRF-TOKEN et X-XSRF-TOKEN.Leçon 1
Un exemple concret
Kitab a une route POST /compte/email qui change l'adresse e-mail de l'utilisateur connecté. Sans protection, un pirate crée sur son site une page piégée :
<form action="https://kitab.ma/compte/email" method="POST">
<input type="hidden" name="email" value="pirate@exemple.com">
</form>
<script>
document.forms[0].submit(); // le formulaire s'envoie dès l'ouverture de la page
</script>pirate@exemple.com. Le pirate demande ensuite un nouveau mot de passe : il prend le contrôle du compte.Pour bloquer cette attaque, l'application doit vérifier, pour chaque requête qui modifie des données (POST, PUT, PATCH, DELETE), une information que le site du pirate ne peut pas connaître.
Leçon 2
Le jeton CSRF
Laravel génère automatiquement un jeton CSRF (token) pour chaque session active. Ce jeton est une longue chaîne aléatoire, stockée dans la session côté serveur. Le site du pirate ne peut pas le lire : le navigateur interdit à une page d'une origine de lire le contenu d'une autre origine.
use Illuminate\Http\Request;
Route::get('/jeton', function (Request $request) {
$jeton = $request->session()->token();
$jeton = csrf_token(); // même valeur, avec l'assistant
return $jeton;
});Chaque formulaire POST, PUT, PATCH ou DELETE doit contenir ce jeton dans un champ caché nommé _token. La directive Blade @csrf le génère :
<form method="POST" action="{{ route('compte.email') }}">
@csrf
{{-- équivaut à : <input type="hidden" name="_token" value="{{ csrf_token() }}"> --}}
<input type="email" name="email">
<button type="submit">Modifier</button>
</form>Le middleware de protection, inclus dans le groupe web, compare le jeton reçu avec celui de la session. S'ils correspondent, la requête vient bien d'un formulaire du site. Sinon, Laravel refuse la requête avec l'erreur 419 Page Expired.
La double protection de Laravel 13
Depuis Laravel 13, le middleware s'appelle PreventRequestForgery et vérifie en deux temps :
Sec-Fetch-Site, qui indique si elle vient de la même origine, du même site ou d'un autre site. Si la requête vient de la même origine, elle est acceptée tout de suite. Cet en-tête n'est envoyé qu'en HTTPS.Dans tous les cas, mets @csrf dans chaque formulaire : en développement (HTTP), c'est le jeton qui protège ; et le code reste compatible avec toutes les versions de Laravel.
Exclure des URI de la protection
Certains services externes envoient des requêtes POST à ton application sans pouvoir connaître le jeton : c'est le cas des webhooks de paiement (Stripe, CMI) qui confirment un paiement. Il faut exclure ces URI. La meilleure solution est de placer ces routes hors du groupe web (par exemple dans routes/api.php). Sinon, on les exclut dans bootstrap/app.php :
->withMiddleware(function (Middleware $middleware): void {
$middleware->preventRequestForgery(except: [
'paiement/webhook', // une URI précise
'stripe/*', // toutes les URI qui commencent par stripe/
]);
})Une route exclue doit alors vérifier autrement que la requête est authentique : les services de paiement signent leurs webhooks avec une clé secrète, que l'application vérifie.
Leçon 3
Leçon 4
| Moyen d'envoyer le jeton | Utilisé par |
|---|---|
champ caché _token (@csrf) | les formulaires HTML |
en-tête X-CSRF-TOKEN (valeur de la balise <meta>) | fetch, jQuery |
en-tête X-XSRF-TOKEN (valeur du cookie XSRF-TOKEN) | Axios, Angular (automatiquement) |
Leçon 5
Leçon 6
Vidéo · À regarder
EN30 Days to Learn Laravel - Complete 8 Hour Course
Laracasts (Jeffrey Way) · YouTube
1Une attaque CSRF exploite…
2Laravel vérifie le jeton CSRF sur les requêtes…
3Le champ caché généré par @csrf s'appelle…
4Un formulaire sans jeton provoque l'erreur…
5En Laravel 13, on exclut une URI avec…
6Avec fetch, le jeton s'envoie dans l'en-tête…
7Axios envoie automatiquement le jeton grâce…
8L'en-tête Sec-Fetch-Site est envoyé par le navigateur…
L'essentiel du chapitre
PreventRequestForgery (groupe web, Laravel 13 ; anciennement VerifyCsrfToken puis ValidateCsrfToken) vérifie l'origine (Sec-Fetch-Site, en HTTPS), sinon le jeton, sur POST, PUT, PATCH, DELETE.@csrf (champ caché _token) ; erreur 419 = jeton absent ou périmé. Jamais d'action qui modifie des données en GET.web, ou $middleware->preventRequestForgery(except: ['stripe/*']) (Laravel 11-12 : validateCsrfTokens) ; vérifier alors la signature du service.<meta name="csrf-token" content="{{ csrf_token() }}"> + en-tête X-CSRF-TOKEN (fetch, jQuery) ; cookie XSRF-TOKEN → en-tête X-XSRF-TOKEN (Axios, Angular, automatique).SameSite=lax, régénération de session à la connexion, password.confirm.Une requête envoyée par JavaScript (AJAX, fetch) n'a pas de champ _token. Le middleware accepte aussi le jeton dans l'en-tête HTTP X-CSRF-TOKEN. On place le jeton dans une balise <meta> du layout :
<meta name="csrf-token" content="{{ csrf_token() }}">Puis le JavaScript lit cette balise et ajoute l'en-tête à chaque requête :
const jeton = document.querySelector('meta[name="csrf-token"]').content;
async function ajouterAuPanier(livreId) {
const reponse = await fetch('/panier', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': jeton,
},
body: JSON.stringify({ livre_id: livreId, quantite: 1 }),
});
const donnees = await reponse.json();
document.querySelector('#nb-articles').textContent = donnees.nb_articles;
}Avec jQuery (code ancien), on configure l'en-tête une fois pour toutes les requêtes :
$.ajaxSetup({
headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') }
});Laravel place aussi le jeton CSRF dans un cookie chiffré nommé XSRF-TOKEN, envoyé avec chaque réponse. Certaines bibliothèques JavaScript, comme Axios et Angular, lisent automatiquement ce cookie et renvoient sa valeur dans l'en-tête X-XSRF-TOKEN pour les requêtes vers la même origine. Avec Axios, il n'y a donc rien à écrire :
import axios from 'axios';
const { data } = await axios.post('/panier', { livre_id: 12, quantite: 1 });
// Axios a ajouté l'en-tête X-XSRF-TOKEN à partir du cookie XSRF-TOKENSameSite. Dans config/session.php, l'option same_site vaut lax par défaut : le navigateur n'envoie pas le cookie de session avec une requête POST provenant d'un autre site. C'est une défense supplémentaire, pas un remplacement du jeton.$request->session()->regenerate() change l'identifiant de session et le jeton, ce qui protège aussi contre la fixation de session.password.confirm.Les formulaires et la protection CSRF expliqués avec des exemples.
(1) Ajoute la balise <meta name="csrf-token"> dans une vue de test. (2) Crée la route POST /panier (nommée panier.ajouter) qui renvoie en JSON ['ok' => true, 'livre_id' => …]. (3) Crée une page avec un formulaire classique (avec @csrf) qui appelle cette route, et vérifie la réponse. (4) Retire @csrf et observe l'erreur. (5) Remets-le, puis ajoute un second bouton qui appelle la même route avec fetch et l'en-tête X-CSRF-TOKEN.
Pour chaque route ou formulaire de Kitab, dis s'il est vulnérable au CSRF et corrige-le : (1) Route::get('/panier/vider', [PanierController::class, 'vider']); (2) un formulaire <form method="POST" action="/avis"> sans @csrf ; (3) Route::post('/paiement/cmi/callback', ...) dans routes/web.php, appelée par la banque, qui renvoie toujours une erreur 419 ; (4) Route::delete('/admin/livres/{livre}', ...) appelée par un bouton dans un formulaire avec @csrf et @method('DELETE').
Sur la fiche d'un livre, un bouton « ♡ Ajouter aux favoris » doit envoyer POST /favoris avec le corps JSON { "livre_id": 7 } sans recharger la page, puis afficher le message renvoyé par le serveur dans <p id="message">. Écris : (1) la balise à placer dans le layout ; (2) le bouton HTML ; (3) le JavaScript avec fetch.
Un collègue dit : « J'ai désactivé la protection CSRF sur tout le site parce que mes formulaires affichaient l'erreur 419. Comme on vérifie déjà que l'utilisateur est connecté avec auth, ce n'est pas grave. » Réponds-lui en quatre ou cinq phrases.