CSRF - Cross-Site Request Forgery : forcer le navigateur d'une victime à agir à sa place
Le CSRF (Cross-Site Request Forgery) exploite la confiance que le serveur accorde aux requêtes émises par le navigateur d'un utilisateur connecté. L'attaquant pousse la victime, à son insu, à exécuter une action sur un site où elle est authentifiée. Le serveur reçoit une requête légitime apparente, avec un cookie de session valide, et l'exécute.
Pourquoi le navigateur est complice
Les navigateurs attachent automatiquement les cookies d'un domaine à toutes les requêtes vers ce domaine, qu'elles viennent du site lui-même ou d'une page tierce. C'est le comportement par défaut, conçu à l'origine pour la commodité des utilisateurs. Le CSRF l'exploite.
Si Alice est connectée sur bank.com et qu'elle visite malicious.com, le navigateur d'Alice enverra ses cookies bank.com pour toute requête vers bank.com, même initiée depuis malicious.com.
Exemples d'attaques
Attaque via une requête GET : Si une action est déclenchée par un GET (mauvaise pratique mais fréquente), une simple image suffit. La victime n'a pas besoin de cliquer.
CSRF via balise img : aucun clic nécessaire
<!-- Inséré dans une page ou un email HTML -->
<!-- Le navigateur charge l'image, déclenchant le virement -->
<img src="https://bank.com/transfer?to=attacker&amount=5000"
width="1" height="1" style="display:none">
Attaque via une requête POST : Pour les actions POST, l'attaquant crée un formulaire HTML caché sur sa page qui se soumet automatiquement au chargement. En quelques millisecondes, la victime a exécuté l'action sans rien voir.
CSRF via formulaire auto-soumis
<!-- Page malicious.com/hack.html -->
<form id="csrf-form"
action="https://bank.com/transfer"
method="POST">
<input type="hidden" name="to" value="attacker-account">
<input type="hidden" name="amount" value="5000">
<input type="hidden" name="currency" value="EUR">
</form>
<script>document.getElementById('csrf-form').submit();</script>
Vecteurs plus discrets : L'attaquant peut aussi utiliser un <iframe> caché pointant vers une page qui déclenche la requête, ou envoyer un email HTML qui charge une image depuis l'URL cible. Les URLs de type fetch() ou XMLHttpRequest cross-origin sont bloquées par le CORS, mais les formulaires HTML natifs et les balises image ne le sont pas.
Ce qui ne suffit pas
Vérifier l'en-tête Referer : Le Referer peut être absent (proxies, extensions de navigateur, paramètre de politique de référence), falsifié dans certains contextes anciens, ou mal validé (une vérification qui accepte bank.com.evil.com ne protège pas). Ce n'est pas une protection fiable seule.
Utiliser POST au lieu de GET : Comme montré ci-dessus, un formulaire auto-soumis exploite aussi les requêtes POST. Le choix de la méthode HTTP ne protège pas contre le CSRF.
L'authentification HTTPS : Le HTTPS protège le canal de transmission, pas l'origine de la requête. Un CSRF fonctionne aussi bien sur HTTPS.
Remédiation
1. Token CSRF synchronisé (Synchronizer Token Pattern) : C'est la protection la plus robuste. Le serveur génère un token aléatoire unique par session (ou par formulaire), le stocke côté serveur, et l'inclut dans chaque formulaire HTML. Lors de la soumission, le serveur vérifie que le token reçu correspond au token attendu. Une page malveillante ne peut pas lire ce token (bloqué par la politique Same-Origin du navigateur).
Token CSRF dans un formulaire HTML
<!-- Token CSRF inclus dans le formulaire -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<input type="text" name="to" placeholder="Bénéficiaire">
<input type="number" name="amount" placeholder="Montant">
<button type="submit">Transférer</button>
</form>
Vérification côté serveur : Express.js (csurf ou implémentation manuelle)
// Génération du token à l'initialisation de la session
import crypto from 'crypto';
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}
// Vérification à la réception du formulaire
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF token invalide' });
}
// Traitement de l'action
});
Pour les APIs JSON : Si votre API accepte uniquement du JSON (Content-Type: application/json), les formulaires HTML natifs ne peuvent pas déclencher un CSRF (ils ne peuvent envoyer que application/x-www-form-urlencoded ou multipart/form-data). Vérifiez explicitement l'en-tête Content-Type côté serveur.
2. Attribut SameSite sur les cookies : Cet attribut indique au navigateur dans quels contextes cross-site il doit envoyer le cookie. C'est la protection la plus simple à mettre en place et la plus transparente pour les utilisateurs.
Configuration des cookies avec SameSite
# SameSite=Strict - le cookie n'est jamais envoyé dans un contexte cross-site
# Protection maximale, mais peut bloquer des liens légitimes depuis d'autres sites
Set-Cookie: session=abc123; SameSite=Strict; HttpOnly; Secure
# SameSite=Lax - le cookie est envoyé uniquement pour les navigations top-level (clic sur lien)
# N'est PAS envoyé pour les requêtes initiées par img, iframe, fetch cross-origin
# Bon compromis utilisabilité / sécurité - valeur par défaut dans les navigateurs récents
Set-Cookie: session=abc123; SameSite=Lax; HttpOnly; Secure
# SameSite=None - envoi dans tous les contextes (compatibilité ancienne)
# Requiert obligatoirement Secure
Set-Cookie: session=abc123; SameSite=None; Secure
SameSite=Lax est aujourd'hui la valeur par défaut dans Chrome et Firefox pour les cookies sans attribut explicite. Cela protège contre la majorité des CSRF courants. Cependant, SameSite=Strict est recommandé pour les applications sensibles.
3. Double Submit Cookie : Alternative aux tokens synchronisés, utile pour les architectures sans état (stateless). Le serveur génère un token aléatoire, le place dans un cookie ET dans un header custom (ou le corps de la requête). Côté serveur, il vérifie que les deux correspondent. Une page malveillante ne peut pas lire ni écrire des cookies d'un autre domaine.
Double Submit Cookie : côté client
// Lire le token depuis le cookie (accessible en JS car pas HttpOnly)
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
// L'envoyer dans un header custom sur chaque requête API
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrf_token') // valeur lue du cookie
},
body: JSON.stringify({ to: 'beneficiary', amount: 500 })
});
4. Vérifier l'en-tête Origin : Pour les requêtes modernes, vérifier l'en-tête Origin (ou Referer en fallback) côté serveur est une couche défense supplémentaire valide, à combiner avec les protections précédentes, pas à utiliser seule.
Vérification de l'en-tête Origin
// Middleware Express.js
app.use((req, res, next) => {
if (req.method !== 'GET' && req.method !== 'HEAD') {
const origin = req.headers.origin || req.headers.referer || '';
if (!origin.startsWith('https://myapp.com')) {
return res.status(403).json({ error: 'Origin non autorisée' });
}
}
next();
});
Récapitulatif des protections
- Activer
SameSite=Strict(ouLaxau minimum) sur tous les cookies de session - Implémenter des tokens CSRF sur tous les formulaires HTML qui déclenchent des actions d'état
- Pour les APIs JSON : vérifier explicitement
Content-Type: application/jsoncôté serveur - Ne jamais déclencher d'actions d'état (modification, suppression, transaction) via des requêtes GET
- Utiliser un framework qui gère les tokens CSRF automatiquement (Rails, Laravel, Django, Spring Security) et ne pas le désactiver
Pour aller plus loin
Une question après la lecture ?
Des doutes sur la protection de vos formulaires ou de vos endpoints d'action ? Envoyez un message.
Ou réservez directement un cadrage de 30 minutes
Réserver un appel →