cyber / cybersecurite-fondamentale
OWASP Top 10 : Cross-Site Request Forgery (CSRF)
Explication
Ce que vous allez apprendre
- Comprendre pourquoi le navigateur joint automatiquement les cookies à chaque requête, même cross-site
- Expliquer le mécanisme du CSRF et pourquoi il cible spécifiquement l'authentification par cookie
- Mettre en place un jeton CSRF (synchronizer token pattern) sur un formulaire sensible
- Utiliser l'attribut de cookie SameSite comme défense moderne et peu coûteuse
- Savoir pourquoi une API authentifiée par Bearer token n'est pas vulnérable au CSRF classique
Dans quel contexte ?
Un utilisateur connecté à sa banque en ligne, dans un onglet, visite ensuite un site de recettes de cuisine dans un autre onglet. Si ce site contient un formulaire caché qui se soumet automatiquement vers banque.example/virement, le navigateur joint le cookie de session bancaire à cette requête sans poser de question. Sans protection CSRF, la banque ne peut pas distinguer cette requête forgée d'un vrai virement voulu par l'utilisateur.
Première étape : un comportement du navigateur qu'on oublie souvent. Quand vous êtes connecté à un site, votre navigateur joint automatiquement votre cookie de session à CHAQUE requête envoyée vers ce site. Cela reste vrai même si la requête part d'une page totalement différente que vous visitez en parallèle.
Deuxième étape : c'est exactement ce que le CSRF exploite. Un site malveillant fait discrètement envoyer une requête vers le site où vous êtes connecté. Votre navigateur y joint fidèlement vos cookies, et le site cible ne peut pas a priori distinguer cette requête d'une action volontaire de votre part.
Pour bien visualiser, une image simple. C'est comme si quelqu'un glissait un formulaire déjà signé de votre écriture dans une pile de courrier officiel. L'institution qui le reçoit voit une signature valide et l'exécute, sans savoir que ce n'est pas vous qui l'avez rédigé consciemment.
Troisième étape : la défense historique, le jeton CSRF. Pendant longtemps, on se défendait avec un jeton unique par session, injecté dans chaque formulaire et vérifié côté serveur à chaque soumission. Ça fonctionne, mais ça demande un effort de développement à chaque formulaire.
Voici la défense moderne, plus simple : l'attribut SameSite. Il traite le problème à la racine, en indiquant au navigateur de NE PAS envoyer le cookie du tout lors d'une requête provenant d'un autre site. Aucun effort supplémentaire par formulaire n'est nécessaire.
| Valeur SameSite | Cookie envoyé en cross-site ? | Usage typique |
|---|---|---|
| Strict | Jamais | Comptes bancaires, actions très sensibles |
| Lax (défaut navigateurs) | Seulement en navigation top-level GET | Compromis standard pour la plupart des sites |
| None | Toujours (nécessite Secure) | Widgets embarqués sur d'autres domaines |
Prérequis
Un point qui surprend souvent les débutants : une API authentifiée uniquement par un jeton Bearer dans l'en-tête Authorization, et non par cookie, n'est pas vulnérable au CSRF classique. Le navigateur n'ajoute jamais automatiquement cet en-tête sur une requête cross-site, seul du JavaScript exécuté sur le domaine légitime peut le faire. Cela ne veut pas dire que ces API sont invulnérables à tout, seulement que le CSRF cible spécifiquement l'authentification par cookie.
Après avoir vu comment un navigateur peut être détourné pour attaquer un serveur, la prochaine leçon inverse complètement la perspective : comment un serveur lui-même peut être détourné pour attaquer d'autres systèmes. C'est le SSRF.
Commandes & code
OWASP Top 10 : Cross-Site Request Forgery (CSRF)
Le CSRF force le navigateur d'une victime authentifiée à envoyer, à son insu, une requête vers un site où elle est connectée (le navigateur joint automatiquement les cookies de session).
<!-- Page malveillante hébergée sur evil.example (démonstration pédagogique en lab) -->
<!-- La victime, connectée sur technologik.local, visite cette page tierce -->
<html>
<body onload="document.forms[0].submit()">
<form action="https://technologik.local/api/account/email" method="POST">
<input type="hidden" name="new_email" value="attaquant@evil.example" />
</form>
</body>
</html>
<!-- Si l'endpoint ne vérifie qu'un cookie de session (sans token CSRF), la requête est acceptée --># Défense 1 : token CSRF (synchronizer token pattern), un secret par session lié au formulaire
import secrets
from fastapi import FastAPI, Request, HTTPException, Depends
app = FastAPI()
def generate_csrf_token(session) -> str:
token = secrets.token_urlsafe(32)
session["csrf_token"] = token
return token
def verify_csrf_token(request: Request, session):
submitted = request.headers.get("X-CSRF-Token") or (request.form and request.form.get("csrf_token"))
expected = session.get("csrf_token")
if not expected or not submitted or not secrets.compare_digest(submitted, expected):
raise HTTPException(status_code=403, detail="Token CSRF invalide ou manquant")# Défense 2 (moderne, recommandée) : cookie SameSite, empêche le navigateur d'envoyer
# le cookie de session lors d'une requête cross-site
Set-Cookie: session=eyJhbGciOi...; HttpOnly; Secure; SameSite=Strict
# SameSite=Strict : cookie jamais envoyé en cross-site (le plus sûr, peut casser des liens externes)
# SameSite=Lax : cookie envoyé pour navigation top-level GET (compromis par défaut des navigateurs)
# SameSite=None : cookie envoyé partout, nécessite obligatoirement Secure (à éviter sauf besoin explicite)# Défense 3 : vérifier l'en-tête Origin / Referer pour les requêtes mutatives (POST/PUT/DELETE)
ALLOWED_ORIGINS = {"https://technologik.local", "https://app.technologik.local"}
@app.middleware("http")
async def csrf_origin_check(request: Request, call_next):
if request.method in ("POST", "PUT", "PATCH", "DELETE"):
origin = request.headers.get("origin") or request.headers.get("referer", "")
if not any(origin.startswith(o) for o in ALLOWED_ORIGINS):
return HTMLResponse("Origine non autorisée", status_code=403)
return await call_next(request)// Côté frontend : envoyer le token CSRF récupéré au chargement de page dans chaque mutation
async function updateEmail(newEmail, csrfToken) {
return fetch("/api/account/email", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken, // requis, sinon 403 côté serveur
},
credentials: "same-origin",
body: JSON.stringify({ new_email: newEmail }),
});
}# Important : une API stateless authentifiée par header Authorization: Bearer <JWT>
# (et NON par cookie) n'est PAS vulnérable au CSRF classique, car le navigateur n'ajoute
# jamais automatiquement un header Authorization sur une requête cross-site.
# Le CSRF concerne spécifiquement l'authentification par COOKIE.Résumé
- Le CSRF exploite l'envoi automatique des cookies par le navigateur sur des requêtes cross-site.
SameSite=Strict/Laxest la défense moderne la plus simple et souvent suffisante.- Le pattern token CSRF (synchronizer token) reste utile en complément ou pour SameSite=None.
- Une API purement Bearer-token (sans cookie) élimine structurellement le risque CSRF classique.
Exercices pratiques
Mission : le virement déclenché depuis un site de recettes
Objectif : Analyser un scénario de CSRF réel et choisir la combinaison de défenses adaptée sans casser les usages légitimes.
Contexte
Un utilisateur connecté à technologik.local (authentifié par cookie de session) visite ensuite un blog de recettes de cuisine dans un autre onglet. Ce blog contient un formulaire caché, invisible à l'écran, dont l'action pointe vers https://technologik.local/api/account/email en méthode POST et se soumet automatiquement au chargement de la page. L'endpoint ne vérifie aujourd'hui que la présence d'un cookie de session valide.