Formulaire HTML auto-soumis conçu par l'attaquant qui envoie une requête POST avec les identifiants de la victime au chargement de la page.
TL;DR
multipart/form-data et application/x-www-form-urlencoded sont des requêtes simples CORS — aucun preflightLe CSRF basé sur POST (CWE-352) est la forme canonique du Cross-Site Request Forgery. Un attaquant héberge une page contenant un formulaire HTML caché ciblant un endpoint modifiant l'état sur l'application authentifiée de la victime. Le formulaire s'auto-soumet via JavaScript au chargement de la page, faisant émettre par le navigateur de la victime une requête POST avec tous les cookies de session valides joints. Le serveur cible reçoit un POST authentifié et correctement structuré et — ne trouvant aucune validation de token CSRF — exécute l'action.
Cette variante reste pertinente malgré SameSite=Lax devenant le défaut Chrome pour les cookies sans attribut explicite. De nombreuses applications utilisent encore SameSite=None explicite (requis pour les intégrations intersites comme les widgets de paiement), et de nombreuses applications héritées n'ont jamais défini d'attribut SameSite. Plus critique encore, même les applications protégées par SameSite=Lax sont vulnérables quand elles n'implémentent pas de tokens CSRF et se reposent sur SameSite seul — puisque SameSite est une défense au niveau du navigateur qui ne protège pas contre les chaînes de contournement sophistiquées.
Le contournement par omission de token est la faille d'implémentation la plus répandue. De nombreux développeurs implémentent la validation du token CSRF comme : « si le champ token est présent, le valider. » Cela crée un contournement : un attaquant qui supprime entièrement le champ token du corps de la requête contourne complètement la validation. La correction correcte est strictement : « rejeter toute requête modifiant l'état qui n'inclut pas un token valide. »
La technique du formulaire auto-soumis exploite la disposition du navigateur à envoyer des requêtes POST encodées en formulaire en interorigine. Contrairement à fetch() et XMLHttpRequest (qui déclenchent un preflight CORS pour les méthodes non simples), les soumissions de formulaires HTML avec application/x-www-form-urlencoded et multipart/form-data sont des « requêtes simples » selon la spécification CORS et ne reçoivent aucun traitement de preflight.
Matrice des requêtes simples CORS du navigateur :
| Content-Type | Déclenche un preflight ? | CSRF possible ? |
|---|---|---|
application/x-www-form-urlencoded | Non | Oui |
multipart/form-data | Non | Oui |
text/plain | Non | Oui (astuce du corps JSON) |
application/json | Oui | Non (sauf CORS mal configuré) |
Tout en-tête personnalisé (ex. X-CSRF-Token) | Oui | Non |
CSRF POST basique — formulaire auto-soumis :
<!-- Page de l'attaquant — la victime visite et soumet instantanément ce formulaire -->
<html>
<body onload="document.getElementById('csrf').submit()">
<form id="csrf" action="https://target.com/compte/mot-de-passe" method="POST"
style="display:none;">
<input type="hidden" name="nouveau_mot_de_passe" value="Attackerpass1!">
<input type="hidden" name="confirmer_mot_de_passe" value="Attackerpass1!">
<!-- Note : aucun champ csrf_token inclus — test du contournement par omission -->
</form>
</body>
</html>CSRF multipart/form-data — pour les endpoints d'upload de fichiers :
<!-- Les endpoints d'upload de fichiers acceptent le multipart — aussi une requête simple -->
<form action="https://target.com/api/profil/avatar" method="POST"
enctype="multipart/form-data">
<input type="hidden" name="action" value="update">
<!-- Omettre le champ fichier — le serveur peut quand même traiter le paramètre action -->
</form>
<script>document.forms[0].submit();</script>Contournement par omission de token — suppression du champ CSRF :
# Validation VULNÉRABLE (omission de token acceptée) :
def validate_csrf(request):
token = request.POST.get("csrf_token")
if token is None:
return # BUG : aucun champ token = sauter entièrement la validation
if token != session["csrf_token"]:
raise Forbidden("Token CSRF invalide")
# Validation SÛRE (présence du token obligatoire) :
def validate_csrf(request):
token = request.POST.get("csrf_token")
if not token or not secrets.compare_digest(token, session["csrf_token"]):
raise Forbidden("Token CSRF manquant ou invalide")| Variante | Technique | Exploite |
|---|---|---|
| Formulaire auto-soumis | onload=form.submit() | Token CSRF manquant ou contournement par omission |
| CSRF multipart | enctype="multipart/form-data" | Requête simple CORS — aucun preflight pour le CSRF d'upload de fichier |
| Omission de token | Supprimer le champ token du corps | Le serveur valide uniquement quand le champ est présent |
| Falsification de token | Soumettre un token aléatoire ou vide | Le serveur utilise une comparaison faible ou un token non lié à la session |
| Token d'un autre utilisateur | Réutiliser le token valide d'un autre utilisateur | Token non lié à la session (pool de tokens global) |
| Double cookie soumis naïf | Injecter un cookie via sous-domaine, correspondre au paramètre | Aucune liaison HMAC sur le schéma de double cookie soumis |
La variante de token d'un autre utilisateur est particulièrement dommageable dans les applications enterprise. Si le serveur valide que le token CSRF soumis existe dans un pool global de tokens valides (plutôt qu'en correspondant au token de la session spécifique), un attaquant authentifié peut récolter son propre token CSRF valide et l'utiliser pour forger des requêtes en tant que n'importe quel autre utilisateur.
Le schéma naïf de double cookie soumis (valeur aléatoire dans le cookie, même valeur dans le paramètre de requête) est vulnérable quand un attaquant contrôle un sous-domaine. L'attaquant injecte un cookie via document.cookie = 'csrf=valeur_attaquant; domain=.target.com' depuis sub.target.com, puis soumet un formulaire avec csrf_param=valeur_attaquant. Sans liaison HMAC, la vérification double-submit du serveur passe.
CVE-2023-47640 — Grails Framework (CVSS 8.8, Élevé) : La protection CSRF intégrée de Grails < 5.3.4 était contournée en omettant le champ token CSRF des requêtes POST. Le framework validait le token uniquement quand le champ était inclus dans la requête, rendant toute la couche de protection CSRF optionnelle pour tout attaquant qui savait supprimer le champ. Chaque endpoint Grails protégé par CSRF était exploitable jusqu'à la mise à jour du framework. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N = 8.8.
CVE-2023-29919 — OpenEMR Patient Portal (CVSS 8.8, Élevé) : CSRF sur l'endpoint de changement d'email dans le portail patient OpenEMR utilisait le même schéma d'omission de token — le champ token n'était validé que quand présent. Un attaquant pouvait changer l'adresse email d'un patient via une page forgée, puis déclencher une réinitialisation de mot de passe vers l'email de l'attaquant, réalisant une prise de contrôle de compte complète avec accès aux informations de santé protégées (PHI). Le vecteur CSRF combiné au flux de réinitialisation de mot de passe élevait la sévérité à une prise de contrôle complète.
CVE-2022-24734 — MyBB Forum (CVSS 8.8, Élevé) : CSRF dans le panneau admin de MyBB permettait l'écriture de fichier contrôlée par l'attaquant via une soumission de formulaire forgée. Cause racine : SameSite=None sur les cookies de session admin (permettant POST en interorigine) combiné à l'absence de validation de token CSRF sur l'action d'upload de fichier. Le vecteur d'écriture de fichier menait à l'exécution de code à distance. Corrigé dans MyBB 1.8.30.
CVE-2024-21690 — Atlassian Confluence CSRF + XSS (CVSS 8.2, Élevé) : Une vulnérabilité combinée CSRF et XSS réfléchi dans Confluence Data Center et Server 7.19.0–8.9.0 permettait à des attaquants non authentifiés à la fois d'injecter du JavaScript arbitraire et de contraindre des utilisateurs authentifiés à effectuer des actions non souhaitées modifiant l'état. Versions affectées jusqu'à 8.9.0 ; corrigé dans 7.19.26+, 8.5.14+, 9.0.1+.
application/x-www-form-urlencoded en multipart/form-data. De nombreux serveurs acceptent les deux ; le multipart peut contourner une validation supplémentaire.Le scanner actif de Burp Suite Pro signale les formulaires sans tokens CSRF. La règle 20012 d'OWASP ZAP (Anti-CSRF Tokens Scanner) identifie les requêtes modifiant l'état sans protection par token. Ni l'un ni l'autre ne teste automatiquement le contournement par omission de token — le scanner actif de Burp soumet le token qu'il trouve dans le formulaire, ne l'omet pas. XSRFProbe est conçu spécifiquement pour les tests d'omission, de token vide et de token entre utilisateurs.
BreachVex teste l'omission de token : après identification des formulaires avec des champs token CSRF via l'analyse HTML, il ressoumet la même requête POST avec le champ token supprimé. Une réponse 2xx sans mots-clés de rejet (ex. « invalid token », « forbidden », « CSRF ») dans le corps constitue un contournement confirmé.
# Rails ApplicationController — protect_from_forgery est ACTIVÉ par défaut
class ApplicationController < ActionController::Base
# NE PAS ajouter : protect_from_forgery except: [:update_email, ...]
# NE PAS ajouter : skip_before_action :verify_authenticity_token
end
# Formulaire sûr — Rails injecte automatiquement authenticity_token
# <%= form_with(url: change_password_path) do |f| %>
# <%= f.password_field :new_password %>
# <%= f.submit "Changer le mot de passe" %>
# <% end %>
# Le mode API Rails (ActionController::API) n'inclut PAS la protection CSRF
# L'ajouter explicitement pour les contrôleurs API authentifiés par cookie de session :
class Api::AccountsController < ActionController::API
include ActionController::RequestForgeryProtection
protect_from_forgery with: :exception
end# settings.py — CsrfViewMiddleware est dans MIDDLEWARE par défaut
# NE PAS ajouter à la liste MIDDLEWARE_CLASSES exclue
# NE PAS utiliser @csrf_exempt sur les vues modifiant l'état
# VULNÉRABLE — exemption explicite
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def change_email(request): # NE PAS FAIRE CELA
...
# SÛRE — s'appuyer sur le middleware (activé par défaut)
def change_email(request):
if request.method == 'POST':
user.email = request.POST['email']
user.save()
return JsonResponse({'ok': True})// Django AJAX — inclure le token CSRF dans les en-têtes
function getCsrfToken() {
return document.cookie.split('; ')
.find(row => row.startsWith('csrftoken='))
?.split('=')[1];
}
fetch('/compte/email', {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: nouvelEmail }),
credentials: 'same-origin',
});// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware {
// VULNÉRABLE : exempter les endpoints de paiement et de profil
// protected $except = ['api/payment/*', 'account/profile']; // NE PAS FAIRE CELA
// SÛRE : tableau $except vide — toutes les routes protégées
protected $except = [];
}{{-- resources/views/compte/email.blade.php --}}
<form action="/compte/email" method="POST">
@csrf
{{-- Se développe en : <input type="hidden" name="_token" value="..."> --}}
<input type="email" name="email" placeholder="Nouvel email">
<button type="submit">Mettre à jour l'email</button>
</form>@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// VULNÉRABLE : de nombreux guides API REST recommandent de désactiver CSRF entièrement
// http.csrf(csrf -> csrf.disable()); // NE PAS FAIRE CELA pour les apps authentifiées par cookie
// SÛRE : CookieCsrfTokenRepository pour les SPA (lisible par JavaScript)
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
);
return http.build();
}
}L'erreur d'implémentation la plus dangereuse est de valider les tokens CSRF uniquement quand ils sont présents. « Token manquant = sauter la vérification » signifie que tout attaquant qui sait omettre le champ contourne l'ensemble de la protection. La logique correcte : token manquant = 403 Forbidden, token invalide = 403 Forbidden, token valide = poursuivre.
L'attaquant crée une page HTML contenant un formulaire caché pointant vers l'endpoint modifiant l'état de la cible. JavaScript déclenche la soumission du formulaire automatiquement quand la victime charge la page. Le navigateur joint les cookies de session de la victime à la requête POST, et le serveur la traite comme si elle avait été légitimement initiée par l'utilisateur.
Oui, SameSite=Lax bloque les soumissions de formulaires POST intersites. C'est la principale raison pour laquelle l'adoption de Lax comme défaut pour les cookies sans attribut SameSite explicite par Chrome a significativement réduit la prévalence du CSRF basé sur POST. Cependant, les applications ont encore besoin de tokens CSRF si elles ne peuvent pas garantir que tous les cookies ont SameSite=Lax ou Strict explicite.
De nombreux frameworks valident un token CSRF uniquement quand le champ token est présent dans la requête. Si le champ est omis entièrement — pas soumis — le serveur saute la validation et accepte la requête. CVE-2023-47640 (Grails, CVSS 8.8) et CVE-2023-29919 (OpenEMR, CVSS 8.8) démontrent tous deux ce schéma : la présence du token est optionnelle, pas obligatoire.
Oui. multipart/form-data est un Content-Type 'simple' selon les règles CORS — il ne déclenche pas de requête OPTIONS de preflight. Un attaquant peut créer un formulaire avec enctype='multipart/form-data' et le POSTer en intersite. Si le serveur accepte des données encodées en multipart sur un endpoint qui utilise normalement JSON ou form-urlencoded, le CSRF est possible.
Omission de token : l'attaquant n'inclut simplement pas du tout le champ token. Si le serveur valide uniquement quand le champ est présent, la requête réussit sans token. Falsification de token : l'attaquant soumet une valeur de token incorrecte. La plupart des serveurs qui implémentent correctement les tokens rejettent la falsification mais peuvent accepter incorrectement l'omission. Les deux doivent retourner HTTP 403.
Intercepter la requête POST légitime modifiant l'état dans Burp Suite. Envoyer au Repeater. Supprimer entièrement le champ csrf_token (ou authenticity_token, _token, __RequestVerificationToken) du corps de la requête. Cliquer sur Envoyer. Si le serveur retourne 200 ou une redirection de succès au lieu de 403, le contournement par omission de token est confirmé.
Django active CsrfViewMiddleware globalement par défaut — chaque requête non sûre nécessite un csrfmiddlewaretoken valide. Rails active protect_from_forgery dans ApplicationController par défaut. Le groupe de middleware web de Laravel inclut VerifyCsrfToken. Spring Security nécessite une configuration explicite (pas activé par défaut pour les APIs REST).
Le tableau $except dans le middleware VerifyCsrfToken de Laravel exempte des routes spécifiques. De nombreux développeurs exemptent des groupes de routes API entiers (api/*) des vérifications CSRF, supposant une authentification JWT ou par clé API. Si ces endpoints acceptent aussi l'authentification par cookie de session (ce que Laravel supporte), les requêtes POST intersites avec un cookie de session réussissent sans token CSRF.