Concept #030
SécuritéCSRF (Cross-Site Request Forgery)
- sécurité
- web
- attaque
Le principe en une phrase
CSRF (Cross-Site Request Forgery) est une attaque où un site malveillant force le navigateur de la victime à envoyer une requête authentifiée vers un autre site, en exploitant le fait que le navigateur attache automatiquement les cookies de session à chaque requête.
Votre navigateur est "trop poli" : il présente vos papiers d'identité (cookies de session) automatiquement à chaque requête vers un domaine, même quand c'est un site malveillant qui lui demande en douce d'effectuer une action sensible.
Anatomie d'une attaque CSRF
Le scénario
- Alice est connectée à sa banque en ligne (
banque.com). Son navigateur détient un cookie de session valide. - Alice visite un site malveillant (
evil.com) -- peut-être un lien reçu par email. - La page de
evil.comcontient un formulaire caché qui s'envoie automatiquement :
<!-- Sur evil.com — invisible pour Alice -->
<form action="https://banque.com/api/transfer" method="POST" id="attack">
<input type="hidden" name="to" value="attacker-iban" />
<input type="hidden" name="amount" value="5000" />
</form>
<script>
document.getElementById("attack").submit();
</script>- Le navigateur d'Alice envoie le formulaire vers
banque.com. Comme Alice est connectée, le navigateur attache automatiquement ses cookies de session à la requête. banque.comreçoit une requête authentifiée et légitime en apparence. Il exécute le virement.
Alice n'a rien cliqué sur banque.com. Elle a simplement visité une page web. C'est la nature automatique de l'envoi des cookies qui rend cette attaque possible.
Variantes de l'attaque
Via une image :
<!-- Requête GET déguisée en image -->
<img src="https://banque.com/api/transfer?to=attacker&amount=5000" />C'est pour cela que les opérations sensibles ne doivent jamais utiliser GET.
Via un lien piégé :
<a href="https://banque.com/api/transfer?to=attacker&amount=5000">
Cliquez ici pour voir la photo
</a>Via un formulaire invisible sur un site compromis :
L'attaquant n'a même pas besoin de son propre site. S'il exploite une faille XSS sur un site populaire, il peut y injecter le formulaire CSRF.
La différence avec le XSS
CSRF et XSS sont souvent confondus mais sont fondamentalement différents :
| CSRF | XSS | |
|---|---|---|
| Principe | Exploite la confiance du serveur envers le navigateur | Exploite la confiance du navigateur envers le serveur |
| Code exécuté | Aucun code malveillant sur le site cible | Code JavaScript malveillant exécuté sur le site cible |
| Ce que l'attaquant peut faire | Effectuer des actions (virement, changement de mot de passe) | Lire des données, voler des sessions, modifier la page |
| Cookies | Envoyés automatiquement par le navigateur | Accessibles via document.cookie (sauf HttpOnly) |
Un point important : le CSRF ne permet pas à l'attaquant de lire la réponse. Il peut déclencher une action, mais il ne voit pas le résultat (grâce à la Same-Origin Policy et CORS). C'est une attaque "en aveugle".
Les défenses contre le CSRF
1. Les tokens CSRF (Synchronizer Token Pattern)
La défense classique. Le serveur génère un token unique et imprévisible, l'intègre dans le formulaire, et le vérifie à la soumission.
import crypto from "crypto";
// Middleware : générer un token CSRF par session
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString("hex");
}
// Rendre le token disponible dans les templates
res.locals.csrfToken = req.session.csrfToken;
next();
});
// Le formulaire HTML inclut le token
// <form method="POST" action="/transfer">
// <input type="hidden" name="_csrf" value="{{csrfToken}}" />
// ...
// </form>
// Middleware de vérification sur les requêtes POST
app.use((req, res, next) => {
if (["POST", "PUT", "DELETE"].includes(req.method)) {
const token = req.body._csrf || req.headers["x-csrf-token"];
if (token !== req.session.csrfToken) {
return res.status(403).json({ error: "Token CSRF invalide" });
}
}
next();
});L'attaquant ne peut pas deviner ce token (il est aléatoire) et ne peut pas le lire sur la page cible (la Same-Origin Policy l'en empêche). Le formulaire malveillant sur evil.com ne peut pas inclure le bon token.
2. L'attribut SameSite des cookies
La protection moderne la plus efficace. L'attribut SameSite contrôle quand le navigateur envoie le cookie :
res.cookie("sessionId", token, {
httpOnly: true,
secure: true,
sameSite: "strict", // ou "lax"
});| Valeur | Comportement |
|---|---|
Strict | Le cookie n'est jamais envoyé sur les requêtes cross-site. Protection totale mais peut gêner la navigation (un lien externe vers votre site ne sera pas authentifié). |
Lax | Le cookie est envoyé sur les navigations de premier niveau (cliquer un lien) mais pas sur les requêtes POST cross-site. Bon compromis. |
None | Le cookie est envoyé partout (nécessite Secure). Aucune protection CSRF. |
Depuis 2020, la plupart des navigateurs utilisent Lax par défaut quand SameSite n'est pas spécifié. C'est une protection par défaut contre les attaques CSRF les plus courantes.
3. Vérification de l'en-tête Origin/Referer
Le serveur peut vérifier que la requête provient bien de son propre domaine :
app.use((req, res, next) => {
if (["POST", "PUT", "DELETE"].includes(req.method)) {
const origin = req.headers.origin || req.headers.referer;
if (!origin || !origin.startsWith("https://banque.com")) {
return res.status(403).json({ error: "Origine non autorisée" });
}
}
next();
});Cette vérification est un filet de sécurité supplémentaire, pas une défense primaire (les headers peuvent être absents dans certains cas).
4. Double Submit Cookie
Une variante sans stockage côté serveur : envoyer le token CSRF à la fois dans un cookie et dans le body/header de la requête. Le serveur vérifie que les deux correspondent.
// Le serveur envoie un cookie CSRF
res.cookie("csrf-token", token, {
httpOnly: false, // le JavaScript doit pouvoir le lire
sameSite: "strict",
secure: true,
});
// Le JavaScript client lit le cookie et l'envoie en header
fetch("/api/transfer", {
method: "POST",
headers: {
"X-CSRF-Token": getCookie("csrf-token"), // lu depuis document.cookie
},
body: JSON.stringify(data),
});Le site malveillant peut déclencher l'envoi du cookie (automatique), mais il ne peut pas lire sa valeur (Same-Origin Policy) et donc ne peut pas l'inclure dans le header.
Les API REST et le CSRF
Les API modernes utilisant des tokens JWT dans le header Authorization (au lieu de cookies) sont naturellement protégées contre le CSRF. Le navigateur n'envoie pas automatiquement le header Authorization -- c'est le code JavaScript qui le fait explicitement.
// Pas de risque CSRF : le token est envoyé manuellement
fetch("/api/transfer", {
method: "POST",
headers: {
Authorization: `Bearer ${jwtToken}`, // pas automatique
"Content-Type": "application/json",
},
body: JSON.stringify({ to: "iban", amount: 1000 }),
});Le CSRF est principalement un problème pour les applications web qui utilisent des cookies de session. Si votre authentification repose uniquement sur des tokens portés dans des headers, le CSRF n'est pas un vecteur d'attaque.
Résumé
Le CSRF exploite un comportement automatique des navigateurs : l'envoi des cookies à chaque requête vers un domaine. La défense repose sur des mécanismes qui prouvent que la requête vient bien de votre propre application : tokens CSRF, attribut SameSite des cookies, vérification de l'origine. Et pour les API modernes utilisant des tokens JWT en headers, le problème ne se pose tout simplement pas.