Passer au contenu principal

Comprendre et se protéger des attaques CSRF (Cross-Site Request Forgery)

Qu’est-ce qu’une attaque CSRF ?

La CSRF (Cross-Site Request Forgery) est une vulnérabilité web qui permet à un attaquant de forcer un utilisateur authentifié à exécuter des actions non désirées sur une application web, sans son consentement.

 

Pourquoi ça fonctionne ?

Les navigateurs web envoient automatiquement les cookies (notamment les cookies de session) avec chaque requête vers un domaine donné, même si cette requête est déclenchée par un site tiers.
Ainsi, si un utilisateur est connecté à bank.demo, il possède un cookie session_id. Si ce même utilisateur visite un site malveillant (par exemple malicious.demo), ce dernier peut déclencher une requête vers bank.demo, et le navigateur inclura le cookie de session comme si la requête venait de l’utilisateur.

 

Exemple d’attaque CSRF

  1. L'utilisateur est connecté à bank.demo. Il possède alors un cookie de session : session_id=abc123

  2. L’attaquant héberge un site piégé : malicious.demo

  3. Ce site contient un formulaire caché comme :

    <form action="https://bank.demo/transfer" method="POST">
      <input type="hidden" name="to" value="attacker" />
      <input type="hidden" name="amount" value="10000" />
      <input type="submit" />
    </form>
    
    <script>
      document.forms[0].submit();
    </script>
  4. L'utilisateur visite malicious.demo pendant qu'il est encore connecté à bank.demo.

  5. Le navigateur exécute le JavaScript, soumet le formulaire vers bank.demo avec le cookie de session.

  6. Si bank.demo n'applique pas de protections CSRF, il pense que la requête est légitime, et effectue le virement.

 

Comment se protéger des attaques CSRF ?

Défense 1 : Attribut SameSite sur les cookies

L'attribut SameSite limite quand un cookie peut être envoyé automatiquement dans une requête inter-sites :

Valeur Description Protection
SameSite=Strict Le cookie n’est jamais envoyé dans une requête initiée par un autre site 🔒 Très forte (mais peut casser des cas d’usage)
SameSite=Lax (valeur par défaut) Le cookie est envoyé pour les navigations GET, mais pas pour les requêtes sensibles (POST/PUT/DELETE) ⚠️ Bonne, mais pas suffisante
SameSite=None Le cookie est envoyé dans toutes les requêtes, même cross-site ❌ Aucune protection sans autre mécanisme

💡 Limite de SameSite=Strict :
Dans les architectures modernes avec une SPA et une API sur des domaines séparés, SameSite=Strict bloque même les requêtes légitimes du frontend vers l’API.

 

Défense 2 : Vérification des en-têtes Origin ou Referer

Le backend peut valider l’origine de chaque requête :

  • Si Origin !== https://bank.demo → Rejeter la requête

  • Si Referer ne commence pas par https://bank.demo → Rejeter aussi

📌 Avantages :

  • Ces en-têtes sont automatiquement ajoutés par le navigateur.

  • Un site externe ne peut pas les falsifier dans une requête HTML ou via JavaScript classique.

⚠️ Limites :

  • Le Referer peut être absent par exemple dans un navigateur configuré en mode privé.

  • Cette méthode ne protège pas contre les attaques manuelles (ex: via curl avec des cookies volés).

curl -X POST https://banque.demo/transfer \
     -H "Origin: https://banque.demo" \
     -H "Cookie: session_id=abc123"

✅ Défense 3 (la plus fiable) : Jeton anti-CSRF (CSRF Token)

Un jeton CSRF est une valeur aléatoire unique générée côté serveur, liée à la session utilisateur.

  • Il est injecté dans le HTML (dans un formulaire ou une balise meta)

  • Le navigateur ne l’envoie pas automatiquement : c’est au client (JavaScript) de l’ajouter explicitement à chaque requête.

Exemple : SPA avec cookies + CSRF Token

fetch('/api/update-profile', {
  method: 'POST',
  credentials: 'include', // envoie les cookies
  headers: {
    'X-CSRF-Token': csrfToken // ajouté volontairement
  },
  body: JSON.stringify({...})
});

Le serveur compare :

  • X-CSRF-Token (reçu)

  • Avec le token stocké en session côté serveur

Si ça ne correspond pas → Requête rejetée (403)

Pourquoi ça protège ?
Parce que le site malveillant ne peut pas lire le HTML ou les données JavaScript de bank.demo (grâce à la politique de même origine). Donc, il ne connaît pas le token et ne peut pas le réutiliser.

Et si on n’utilise pas de cookie ?

Authentification par JWT dans le header Authorization

Si tu n’utilises pas de cookies, mais que tu stockes le token JWT côté client et que tu l’envoies manuellement :

fetch('/api/protected', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer <token>',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({...})
});

➡️ Aucune vulnérabilité CSRF possible, car le navigateur ne fait rien automatiquement.

⚠️ Mais attention :

  • Si tu stockes le JWT dans localStorage ou sessionStorage, il peut être volé via une attaque XSS.

  • Il est donc essentiel de se prémunir contre les scripts malveillants en appliquant des règles strictes de Content Security Policy (CSP).

Résumé : Quand faut-il un token CSRF ?

Type d’authentification CSRF possible ? CSRF Token requis ? Pourquoi ?
Cookie de session (session_id) ✅ Oui ✅ Oui Cookies envoyés automatiquement
JWT dans cookie (HttpOnly) ✅ Oui ✅ Oui Même problème que les sessions
JWT dans header Authorization ❌ Non ❌ Non Rien n’est envoyé automatiquement
JWT dans localStorage ou mémoire JS ❌ Non ❌ Non Requêtes manuelles, pas CSRF

Bonnes pratiques

  • Utilise au minimum SameSite=Lax sur les cookies

  • Active la protection CSRF pour toutes requêtes modifiantes (POST/PUT/DELETE)

  • Ne mets jamais le token CSRF dans un cookie sinon, il sera envoyé automatiquement → la protection devient inefficace.

  • Ne te fie pas uniquement à Origin ou Referer, surtout pour des opérations sensibles

  • Préfère l’auth avec Authorization: Bearer (sans cookie) pour éliminer totalement les risques CSRF

  • Protège-toi contre le XSS si tu stockes des tokens dans localStorage