Tous les articles

SameSite contre les tokens CSRF, faut-il garder le token

CentralCSP Team ·

Dernière mise à jour:

Non. SameSite ne remplace pas les tokens CSRF. L'attribut de cookie est une vraie défense en profondeur contre le cross-site request forgery, mais il est limité au site, le défaut du navigateur n'est pas appliqué partout, et son mode assoupli laisse encore passer certaines requêtes cross-site. La spécification des cookies elle-même dit que l'application de Lax n'est pas une défense complète contre le CSRF et renvoie les développeurs vers un mécanisme de gestion de session pour une couverture totale. Posez SameSite explicitement et gardez vos tokens CSRF. Cet article explique ce que chaque couche bloque, les quatre contournements documentés de SameSite, et pourquoi les deux défenses couvrent mutuellement leurs failles.

Ce qu'est le CSRF, et pourquoi les cookies le rendent possible

Le cross-site request forgery (CSRF) est une attaque où une page que l'attaquant contrôle fait envoyer par le navigateur de la victime une requête qui change l'état vers votre site. La victime est déjà connectée, donc la requête transporte sa session et le serveur agit dessus : virer de l'argent, changer un e-mail, supprimer un compte.

La raison pour laquelle cela fonctionne, c'est que le navigateur attache vos cookies à une requête automatiquement, selon la destination de la requête, pas selon sa provenance. La page d'un attaquant ne peut pas lire votre cookie de session, mais elle peut déclencher une requête vers votre site (un auto-envoi de formulaire, un chargement d'image, un fetch), et le navigateur inclut obligeamment le cookie de session quand même. Cet attachement automatique est toute la vulnérabilité. Les deux défenses ci-dessous existent pour la briser.

Ce que SameSite bloque, et ce que Lax laisse encore passer

SameSite est un attribut Set-Cookie qui indique au navigateur quand un cookie peut accompagner des requêtes cross-site. Il a trois valeurs.

  • SameSite=Strict : le cookie n'est envoyé que sur des requêtes same-site. Une requête provenant de tout autre site ne le transporte jamais, y compris un clic de lien normal depuis un e-mail ou un autre site. La session est intacte, mais cette première navigation arrive sans le cookie, donc la page s'affiche comme si l'utilisateur était déconnecté.
  • SameSite=Lax : le cookie est envoyé same-site, plus sur les navigations de premier niveau cross-site qui utilisent une méthode sûre (un clic de lien GET ou un formulaire GET). Une navigation de premier niveau est celle qui change la barre d'adresse, donc un fetch cross-site, un <img>, un <script> ou une requête <iframe> ne transporte jamais le cookie.
  • SameSite=None : le cookie est envoyé sur chaque requête, cross-site incluse. La spécification des cookies exige que None soit accompagné de Secure ; Chromium et Firefox rejettent un cookie None sans lui, tandis que Safari l'accepte actuellement.

Lax stoppe le vecteur CSRF le plus courant, le POST cross-site caché, parce qu'un POST cross-site n'est pas une navigation de premier niveau sûre. Ce que Lax laisse encore passer, c'est le GET de premier niveau cross-site. Si une requête GET sur votre site change l'état (elle ne le devrait pas, mais beaucoup le font), Lax n'empêchera pas un attaquant de naviguer le navigateur de la victime vers elle.

Notez aussi ce que SameSite ne touche jamais : les requêtes same-site. Les trois valeurs ne gouvernent que l'envoi cross-site. Toute requête déjà same-site, y compris un fetch initié en JavaScript depuis n'importe quelle page de votre site, transporte le cookie en entier. Cela compte plus bas.

Les failles qui gardent les tokens nécessaires

SameSite rétrécit la surface d'attaque. Il ne la referme pas, pour quatre raisons documentées. Elles correspondent aux classes de contournement que PortSwigger catalogue pour les restrictions SameSite.

SameSite est limité au site, pas à l'origine. Un « site » dans la spécification des cookies est le schéma plus le domaine enregistrable, donc https://a.example.com et https://b.example.com sont le même site, tandis que http://example.com et https://example.com ne le sont pas. Un cookie posé avec n'importe quelle valeur SameSite est envoyé sur les requêtes provenant de n'importe quel sous-domaine frère. La formulation de PortSwigger mérite d'être retenue : une requête peut rester same-site même si elle est émise cross-origin. Un bug XSS ou une redirection ouverte sur n'importe quel sous-domaine de votre domaine enregistrable devient un gadget same-site ; les requêtes lancées depuis lui transportent tous les cookies du site, et SameSite n'apporte rien. Un token CSRF exige toujours que l'attaquant obtienne la valeur du token. (Un XSS complet sur la même origine que le formulaire peut en général lire le token aussi, donc les tokens atténuent surtout le cas du sous-domaine frère plutôt que le XSS same-origin ; le XSS demande ses propres défenses, à commencer par une Content Security Policy.)

Le défaut du navigateur n'est pas appliqué partout. Les navigateurs basés sur Chromium traitent un cookie sans attribut SameSite comme Lax. Firefox n'a livré ce comportement que derrière un flag, jamais dans une version stable, et a désactivé même son expérimentation Nightly en 2023. Safari n'applique aucun défaut. Dans ces navigateurs, un cookie de session sans attribut accompagne chaque requête cross-site, exactement le comportement d'avant 2020. Se fier au défaut, c'est se fier au navigateur que votre utilisateur se trouve exécuter. Posez l'attribut vous-même.

Les cookies sans attribut bénéficient dans Chromium d'une exception de deux minutes pour les POST cross-site. Quand Chromium applique le défaut Lax à un cookie, il envoie quand même ce cookie sur les requêtes POST de premier niveau cross-site pendant les deux premières minutes après la création du cookie, une intervention annoncée comme temporaire en 2019 et toujours présente dans le source de Chromium en 2026. PortSwigger documente des attaques qui rouvrent la fenêtre en forçant le navigateur de la victime à récupérer un cookie de session frais, puis en tirant le POST de premier niveau forgé dans les 120 secondes. L'exception ne s'applique qu'aux cookies sans attribut ; un cookie posé avec un SameSite=Lax explicite ne l'obtient jamais. Une raison de plus de poser la valeur plutôt que de s'appuyer sur le défaut.

Un GET qui change l'état reste exposé sous Lax. Lax permet par conception les navigations GET de premier niveau cross-site. Cela couvre non seulement les endpoints qui détournent GET, mais aussi les frameworks qui honorent un paramètre de substitution de méthode (le _method de Symfony, par exemple), qui laissent un GET permis par Lax atteindre une route POST côté serveur. Une vérification de token ne se soucie pas de la méthode, donc elle couvre les deux cas.

Pourquoi HttpOnly n'aide pas contre le CSRF

HttpOnly vaut la peine d'être posé, mais il résout un problème différent. Il empêche JavaScript de lire le cookie (document.cookie), ce qui bloque un payload de cross-site scripting (XSS) qui volerait la valeur de session. Il ne fait rien contre le CSRF, parce que le CSRF n'a jamais besoin de lire le cookie. Le navigateur attache toujours un cookie HttpOnly à la requête forgée automatiquement, ce qui est exactement le comportement dont l'attaque abuse.

SameSite vs tokens CSRF, ce que chaque couche bloque

Les tokens CSRF utilisent une valeur que l'attaquant ne peut pas fournir. Le serveur génère un token imprévisible, l'intègre dans la page ou le formulaire, et rejette toute requête qui change l'état sans le renvoyer. C'est la recommandation principale d'OWASP pour les applications avec état (le pattern du token synchronisé), avec SameSite traité comme une couche de défense en profondeur par-dessus, pas comme un remplaçant. La page cross-site d'un attaquant ne peut pas lire le token (la same-origin policy le bloque), donc elle ne peut pas forger une requête qui passe la vérification.

Chemin d'attaqueCouche SameSiteCouche token CSRF
POST cross-site depuis un formulaire cachéBloqué par Lax et StrictBloqué (pas de token valide)
Requête de sous-ressource cross-site (fetch, img, script)Bloqué par Lax et Strict (pas une navigation de premier niveau)Bloqué (pas de token valide)
GET de premier niveau cross-site qui change l'étatAutorisé sous LaxBloqué (pas de token valide)
Substitution de méthode (_method) derrière un GET de premier niveauAutorisé sous LaxBloqué (pas de token valide)
Requête forgée depuis un sous-domaine frèreNon bloqué (même site)Bloqué (pas de token valide)
Utilisateur sur Safari ou Firefox avec un cookie sans attributNon bloqué (pas de défaut Lax)Bloqué (pas de token valide)
Cookie sans attribut dans la fenêtre POST de deux minutes de ChromiumNon bloquéBloqué (pas de token valide)
Vol de cookie via XSSCe n'est pas son rôleCe n'est pas son rôle

Le schéma est constant : SameSite est un bon mur extérieur que certaines requêtes contournent, et le token est la vérification qui ne dépend ni de l'origine de la requête, ni de la méthode de requête, ni du comportement du navigateur.

Posez SameSite explicitement sur votre cookie de session, aux côtés de Secure et HttpOnly, en utilisant le préfixe host-only __Host- pour un identifiant de session.

Set-Cookie: __Host-session=8f3k...; Secure; HttpOnly; SameSite=Lax; Path=/

Utilisez Strict si arriver déconnecté après un lien externe est acceptable ; utilisez Lax quand vous avez besoin que les liens entrants gardent la session. Puis gardez un token sur chaque requête qui change l'état. Le token vit dans le formulaire ou un header de requête, et le serveur le valide côté serveur.

<form method="POST" action="/account/email">
  <input type="hidden" name="csrf_token" value="{{ server_generated_token }}">
  <input type="email" name="email">
  <button type="submit">Save</button>
</form>

Recommandation

Posez SameSite explicitement (Strict de préférence, Lax quand vous avez besoin que les GET de premier niveau cross-site gardent la session) et ne vous fiez jamais au défaut du navigateur. Posez aussi Secure et HttpOnly, et préférez le préfixe __Host- pour les identifiants de session. Par-dessus, gardez des tokens CSRF sur chaque endpoint qui change l'état, ce qui est le socle OWASP. SameSite rétrécit l'attaque, le token referme les cas qu'il laisse ouverts, et ensemble ils couvrent les failles l'un de l'autre. Vérifiez les attributs Set-Cookie que vous déployez réellement avec le scanner de headers de sécurité gratuit de CentralCSP, qui rapporte les flags SameSite, Secure et HttpOnly sur chaque cookie qu'une page pose.

FAQ

SameSite suffit-il à empêcher le CSRF ?

Non. OWASP classe SameSite comme un contrôle de défense en profondeur qui ne remplace pas une défense CSRF dédiée, et la spécification des cookies elle-même dit que l'application de Lax n'est pas une défense complète contre le CSRF en tant que catégorie. Il est limité au site (les sous-domaines frères le contournent), pas appliqué par défaut dans Safari ni dans Firefox stable, et Lax permet encore les GET de premier niveau cross-site.

Ai-je encore besoin de tokens CSRF si je pose SameSite Lax ?

Oui, pour presque toutes les applications. La recommandation principale d'OWASP pour les applications avec état est le pattern du token synchronisé, avec SameSite en couche par-dessus. Les tokens ne dépendent ni des défauts du navigateur, ni du périmètre de site, ni de la méthode de requête, donc ils couvrent chaque faille que Lax laisse.

SameSite Lax ou Strict, lequel vaut mieux contre le CSRF ?

Strict n'envoie jamais le cookie cross-site, donc il bloque aussi le cas du GET qui change l'état, mais les utilisateurs arrivant d'un lien externe atterrissent sur une vue déconnectée. Lax garde les liens entrants fonctionnels et laisse forgeable le GET qui change l'état (ou une substitution de méthode derrière lui). Aucune des deux valeurs ne protège contre un attaquant capable d'émettre des requêtes same-site.

Quels sont les contournements connus de SameSite ?

Quatre classes documentées. Les GET de premier niveau cross-site sous Lax, y compris les substitutions de méthode des frameworks. La fenêtre de deux minutes de Chromium pour les POST cross-site avec des cookies sans attribut SameSite. Les gadgets same-site, forger depuis un sous-domaine frère via un XSS ou une redirection ouverte. Et les navigateurs sans défaut Lax (Safari, Firefox stable), qui envoient un cookie sans attribut sur chaque requête cross-site.

Lectures liées

Sources