Contourne la protection du cookie SameSite=Lax en utilisant des sous-domaines same-site, des navigations de premier niveau ou des redirections intra-site.
TL;DR
sub.target.com → les requêtes vers target.com sont same-site, tous les cookies envoyéstarget.com/redirect?url=/compte/delete convertit intersite en same-siteOrigin: null — les serveurs permissifs l'autorisentSameSite=Strict + préfixe __Host- + tokens CSRF + en-têtes Fetch MetadataLe contournement SameSite CSRF (CWE-352) décrit les attaques qui contournent l'attribut de cookie SameSite — la principale défense moderne contre le Cross-Site Request Forgery. SameSite contrôle quand les cookies sont inclus dans les requêtes intersites. Quand les applications s'appuient sur SameSite comme leur unique défense CSRF (omettant les tokens synchroniseurs et les vérifications Fetch Metadata), tout contournement de l'application SameSite les rend complètement vulnérables.
Il existe cinq chemins de contournement distincts à ce jour (2024–2026). Chacun cible une valeur SameSite différente ou un mécanisme de navigateur différent. La fenêtre Lax+POST de 120 secondes est unique à Chrome et s'applique uniquement aux cookies sans attribut SameSite explicite — mais puisque seulement 3,52 % des sites définissent un attribut SameSite, cette fenêtre affecte la grande majorité des applications en production. Le contournement par domaine frère fonctionne contre toutes les valeurs SameSite simultanément parce que la définition de « same-site » est plus large que « same-origin ».
La défense en profondeur est obligatoire. SameSite=Strict + tokens CSRF + en-têtes Fetch Metadata ensemble réduisent la surface de contournement viable à quasi zéro. Toute défense unique isolément a des chemins de contournement connus.
SameSite=Lax est conçu pour bloquer les soumissions de formulaires intersites tout en autorisant la navigation naturelle. Il autorise explicitement les cookies lors de la navigation GET de premier niveau — quand le navigateur change l'URL dans la barre d'adresse via un lien, window.location ou meta refresh. Les endpoints GET modifiant l'état sont donc entièrement exploitables même contre des cookies de session protégés par SameSite=Lax.
<!-- Navigation de premier niveau — le navigateur envoie les cookies SameSite=Lax -->
<script>window.location = 'https://target.com/compte/email?email=attaquant@evil.com';</script>
<!-- Ou via meta refresh — aucun JavaScript requis -->
<meta http-equiv="refresh" content="0;url=https://target.com/virement?vers=attaquant&montant=5000">
<!-- Ou via remplacement de méthode HTTP — GET traité comme POST par le framework -->
<!-- Symfony/Rails/Laravel : ?_method=POST ou X-HTTP-Method-Override: POST -->
<a href="https://target.com/compte/mot-de-passe?_method=POST&nouveau_mdp=hacke123">
Réclamez votre prix
</a>Le remplacement de méthode mérite une attention particulière. Le paramètre _method de Symfony, le middleware method_override de Rails et le champ _method de Laravel permettent à une requête GET d'être traitée comme POST, PUT ou DELETE. SameSite=Lax envoie les cookies avec la méthode de transport GET. Le serveur traite le POST remplacé. Lax est contourné.
Chrome applique SameSite=Lax aux cookies qui omettent l'attribut, mais accorde une période de grâce de 120 secondes où les requêtes POST intersites sont autorisées pour les cookies fraîchement émis. C'est une heuristique au niveau du navigateur, non spécifiée dans RFC 6265bis, conçue pour éviter de casser les flux SSO qui émettent des cookies de session puis POSTent en intersite.
Timing de l'attaque :
T+0s : L'attaquant force une navigation de premier niveau vers target.com
→ La cible émet un nouveau cookie de session (SameSite=Lax par défaut, sans attribut explicite)
→ La période de grâce de 120 secondes commence
T+0,5s : L'attaquant redirige la victime vers la page de payload CSRF (popup ou iframe)
→ La page soumet un POST intersite vers l'endpoint modifiant l'état
T+1s : Le navigateur inclut le cookie Lax dans le POST intersite (dans la période de grâce)
→ Le serveur exécute le changement d'état
→ CSRF réussit malgré la « protection » SameSite=LaxStructure du PoC :
// Étape 1 : Forcer le rafraîchissement du cookie via navigation de premier niveau vers la cible
window.open('https://target.com/'); // déclenche la ré-émission du cookie
// Étape 2 : Après un court délai, soumettre le formulaire CSRF
setTimeout(() => {
document.getElementById('csrf-form').submit();
}, 500); // Bien dans la fenêtre de 120 secondes
// La contrainte critique :
// - Ne fonctionne que pour les cookies SANS attribut SameSite explicite
// - SameSite=Lax explicite N'EST PAS soumis à la période de grâce
// - Cette distinction est pourquoi SameSite=Lax explicite est plus fort que le défautLa fenêtre de 2 minutes distingue SameSite=Lax (explicite, défini par le développeur) de SameSite=Lax (implicite, défaut du navigateur pour les cookies sans attribut). Seul le défaut implicite porte la période de grâce. Définir SameSite=Lax explicitement élimine cette fenêtre. Définir SameSite=Strict élimine à la fois la fenêtre et tous les vecteurs de navigation GET.
SameSite=Strict bloque toutes les requêtes intersites, y compris la navigation de premier niveau. Cependant, si le domaine cible a une redirection ouverte côté serveur ou une redirection JS côté client utilisant une entrée contrôlée par l'attaquant, les requêtes peuvent être escaladées d'intersite à same-site :
Étape 1 : L'attaquant envoie la victime vers : https://target.com/redirect?url=/compte/email?email=attaquant@evil.com
Étape 2 : L'endpoint /redirect de target.com effectue : 302 Location: /compte/email?email=attaquant@evil.com
Étape 3 : Le navigateur suit le 302 — c'est maintenant une requête same-site (origine : target.com)
Étape 4 : Le navigateur envoie les cookies SameSite=Strict (requête same-site)
Étape 5 : /compte/email traite le changement d'étatLe mécanisme clé : après la première redirection, le navigateur considère toutes les requêtes suivantes comme provenant de target.com (same-site), même si la chaîne a démarré depuis evil.com. Les redirections 3xx côté serveur changent l'origine perçue de la chaîne.
Les redirections côté client utilisant window.location = document.URL.searchParams.get('url') produisent le même résultat. Ces « gadgets » existent dans pratiquement toutes les grandes applications web — chercher redirect_uri, return_url, next, continue, destination, url dans le codebase de la cible.
La définition du navigateur de « same-site » est eTLD+1 (effective top-level domain + 1 label), pas l'origine. Tous les sous-domaines de target.com sont same-site avec target.com. Un script s'exécutant sur assets.target.com, api.target.com ou cdn.target.com peut émettre des requêtes vers app.target.com que le navigateur classifie comme same-site — contournant toutes les restrictions SameSite.
// Script s'exécutant sur assets.target.com (via XSS, prise de contrôle de sous-domaine, ou mauvaise configuration CORS)
// Cette requête est SAME-SITE avec app.target.com
fetch('https://app.target.com/compte/admin', {
method: 'POST',
credentials: 'include', // inclut les cookies SameSite=Strict
body: JSON.stringify({ role: 'admin' }),
headers: { 'Content-Type': 'application/json' },
});
// Tous les cookies SameSite pour target.com sont inclus — Strict, Lax et NoneCette chaîne de contournement : (1) trouver du XSS sur n'importe quel sous-domaine de la cible, (2) utiliser le XSS pour émettre des requêtes same-site vers l'application principale. Le préfixe de cookie __Host- atténue contre l'injection de cookie par sous-domaine mais pas contre les requêtes same-site cross-sous-domaine — l'authentification de requête repose encore sur des cookies valides sur l'ensemble de l'eTLD+1.
Les navigateurs envoient Origin: null depuis les iframes sandbox (<iframe sandbox="allow-scripts">), certaines URLs data: et contextes file://. Si la validation d'origine du serveur accepte null ou les origines absentes (une erreur courante), l'iframe sandbox peut POSTer en intersite sans attribution d'origine :
<!-- L'iframe sandbox — envoie Origin: null, pas Origin: evil.com -->
<iframe sandbox="allow-scripts allow-forms" srcdoc="
<form method='POST' action='https://target.com/compte/email'>
<input type='hidden' name='email' value='attaquant@evil.com'>
<script>document.forms[0].submit();</script>
</form>
">Le serveur reçoit Origin: null. Une vérification d'origine vulnérable :
# Validation d'origine VULNÉRABLE
def check_origin(request: Request):
origin = request.headers.get("Origin")
if not origin or origin == "null":
return # BUG : null ou absent traité comme autorisé
if not origin.startswith("https://target.com"):
raise Forbidden("Origine invalide")La correction : traiter Origin: null comme une source cross-origin non fiable et la rejeter pour les requêtes modifiant l'état.
| Contournement | Cible | Technique | SameSite contourné |
|---|---|---|---|
| Navigation GET de premier niveau | Lax, None, Absent | window.location, <a>, meta refresh | Lax (nav GET autorisée) |
| Remplacement de méthode | Lax | ?_method=POST, astuce framework | Lax (transport GET) |
| Fenêtre de 120 secondes | Absent (défaut Lax) | Déclencher rafraîchissement cookie + POST immédiat | Lax défaut implicite |
| Chaîne de redirection ouverte | Strict, Lax, None | Gadget de redirection côté serveur ou client | Tous (same-site après redirection) |
| XSS de domaine frère | Strict, Lax, None | XSS sur n'importe quel sous-domaine de l'eTLD+1 | Tous (définition same-site) |
| Origin null | Tout | Iframe sandbox | Validation d'origine côté serveur |
Lab PortSwigger — « SameSite Lax bypass via cookie refresh » : Démontre l'exploit de la fenêtre de 120 secondes. Le lab force une navigation de premier niveau pour rafraîchir le cookie de session, puis soumet immédiatement un POST intersite qui réussit parce que le cookie est dans sa période de grâce. Publié dans PortSwigger Web Security Academy comme comportement de navigateur confirmé, pas une attaque théorique.
Lab PortSwigger — « SameSite Lax bypass via method override » : Le paramètre _method de Symfony exploité pour déclencher une action équivalente à POST via requête GET. Les cookies SameSite=Lax sont envoyés avec le GET. Le framework traite la méthode remplacée. Cela s'applique à toute application Symfony, Rails ou Laravel avec le remplacement de méthode activé.
CVE-2024-4994 — GitLab GraphQL (CVSS 8.1) : La chaîne d'exploit CSRF de GitLab utilisait des Content-Types qui contournaient le preflight CORS — lui-même une forme de contournement adjacent à SameSite où le mécanisme CORS était censé servir de protection CSRF. HackerOne #1122408 a documenté le vecteur de mutation basé sur GET qui contournait toutes les restrictions SameSite via l'interface GET GraphQL.
HackerOne #1860380 — Acronis PUT CSRF (600 $) : Attaque chaînée combinant traversée de chemin côté client pour atteindre le chemin cible, bombe de cookies pour évincer les cookies légitimes, et PUT-CSRF — exploitant la définition d'origine same-site pour contourner la protection SameSite sur le sous-domaine cible.
Set-Cookie pour tous les cookies de session. Si SameSite est absent ou None, l'application n'a pas de protection SameSite.?_method=POST aux requêtes GET pour les endpoints POST. Tester l'en-tête X-HTTP-Method-Override: POST. Une réponse 2xx confirme que le framework accepte le remplacement de méthode.redirect, return_url, next, destination, continue. Tester s'ils redirigent vers des chemins ou domaines arbitraires.Origin: null dans Burp Repeater. Si le serveur retourne 2xx sans erreur, Origin null est acceptée.Les scanners automatisés couvrent rarement les chaînes de contournement SameSite — elles nécessitent une logique d'exploitation en plusieurs étapes. BreachVex détecte le contournement de changement d'état GET (Contournement 1) via une sonde différentielle. La fenêtre de 120 secondes nécessite des tests scriptés avec conscience du timing. Le contournement par domaine frère nécessite une énumération des sous-domaines combinée au scan XSS — une chaîne en plusieurs étapes que la cartographie de surface d'attaque de BreachVex supporte.
Aucun contrôle unique ne couvre les cinq chemins de contournement. Implémenter les trois couches :
Couche 1 : SameSite=Strict (explicite, pas par défaut)
Couche 2 : Token synchroniseur (lié à la session, CSPRNG, 128 bits)
Couche 3 : Politique d'isolation de ressources Fetch MetadataConfiguration des cookies :
Set-Cookie: __Host-SID=<token>; Path=/; Secure; HttpOnly; SameSite=StrictLe préfixe __Host- est critique : il empêche les sous-domaines d'injecter un cookie de remplacement pour le schéma de double-submit. Sans __Host-, un sous-domaine compromis peut définir SID=valeur_attaquant; domain=.target.com.
SameSite=Strict explicite dans le code :
# FastAPI — définir SameSite=Strict explicite (élimine la fenêtre de 120 secondes)
response.set_cookie(
key="__Host-SID",
value=session_id,
httponly=True,
secure=True,
samesite="strict", # Explicite — élimine la période de grâce ; utiliser 'lax' seulement si les flux OAuth le requièrent
path="/",
)Application des Fetch Metadata :
# Rejeter les requêtes modifiant l'état intersites au niveau serveur
async def fetch_metadata_guard(request: Request):
site = request.headers.get("Sec-Fetch-Site", "")
mode = request.headers.get("Sec-Fetch-Mode", "")
method = request.method
if method in ("GET", "HEAD", "OPTIONS"):
return # Les méthodes sûres toujours autorisées
if site in ("same-origin", "same-site", "none"):
return # Same-origin et nav directe toujours autorisés
if mode == "navigate" and site == "cross-site":
return # Navigation de premier niveau (liens) autorisée — nécessaire pour les callbacks OAuth
# Requêtes non-navigation intersites vers des méthodes modifiant l'état → rejeter
raise HTTPException(status_code=403, detail="Requête intersite rejetée par la politique d'isolation")Validation d'origine (repli pour les anciens navigateurs) :
def validate_origin(request: Request):
origin = request.headers.get("Origin") or request.headers.get("Referer", "")
if not origin:
# Origine absente — autoriser seulement si Sec-Fetch-Site est appliqué ci-dessus
return
if origin == "null":
# Iframe sandbox — traiter comme intersite, rejeter pour les requêtes modifiant l'état
raise HTTPException(status_code=403, detail="Origine null rejetée")
if not (origin.startswith("https://target.com") or origin.startswith("https://www.target.com")):
raise HTTPException(status_code=403, detail="Origine invalide")La vulnérabilité de correspondance partielle de l'en-tête Origin est une erreur d'implémentation courante : startsWith("https://target.com") passe pour https://target.com.evil.com. Utiliser l'égalité exacte ou la vérification endsWith avec la liste d'origines connues. Ne jamais utiliser la correspondance de sous-chaîne partielle contre des entrées non fiables.
Chrome applique SameSite=Lax aux cookies sans attribut SameSite explicite mais accorde une période de grâce de 120 secondes où les requêtes POST intersites sont autorisées après qu'un cookie est fraîchement défini. Un attaquant force une navigation de premier niveau vers la cible (ex. via redirection OAuth ou déconnexion/reconnexion), déclenchant un nouveau cookie. Dans les 120 secondes, un POST intersite vers un endpoint modifiant l'état réussit avec le nouveau cookie Lax joint.
Non. SameSite=Strict bloque toutes les requêtes intersites, y compris la navigation de premier niveau. Cependant, il peut être contourné via un gadget de redirection côté client sur le domaine cible : visiter target.com/redirect?url=/compte/delete fait que le navigateur émet la deuxième requête comme same-site, envoyant les cookies Strict. Les redirections ouvertes et les redirections JS côté client (window.location = param) sont les principaux gadgets de contournement.
Same-site est défini au niveau eTLD+1 (domaine enregistrable), pas de l'origine. Un XSS sur un sous-domaine assets.target.com permet des requêtes vers app.target.com qui sont classifiées comme same-site par le navigateur. Les cookies SameSite=Strict et Lax sont tous deux envoyés sur les requêtes same-site, indépendamment des différences de sous-domaine. Cela contourne complètement les protections SameSite sur toute l'organisation.
Les navigateurs envoient Origin: null depuis des iframes sandbox (iframe avec attribut sandbox) et certains contextes file:// ou data:. Si le serveur a une logique permissive vérifiant 'si Origin est null ou absent, autoriser', une iframe sandbox sur la page de l'attaquant peut POSTer vers la cible sans être bloquée. Le navigateur envoie Origin null au lieu de evil.com.
Les frameworks comme Symfony, Rails et Laravel acceptent un paramètre _method=POST en query string ou un en-tête X-HTTP-Method-Override dans les requêtes GET. Un attaquant envoie une requête GET (que SameSite=Lax autorise lors de la navigation de premier niveau) avec ?_method=POST, et le framework traite la requête comme un POST, exécutant le changement d'état tandis que le navigateur envoie les cookies Lax parce que la méthode de transport est GET.
Forcer une reconnexion via navigation de premier niveau (ce qui réinitialise le cookie). Immédiatement (dans les 2 minutes) soumettre un formulaire POST intersite vers un endpoint modifiant l'état. Si le POST réussit avec une réponse 2xx, la fenêtre de grâce Lax+POST est exploitable. La contrainte de timing signifie que cela nécessite une exploitation scriptée plutôt que des tests manuels dans la plupart des cas.
Un audit 2024 a révélé que seulement 3,52 % des sites web implémentent l'attribut SameSite. Cela signifie que 96,48 % des applications ont encore des cookies de session sans protection SameSite, rendant la gamme complète d'attaques CSRF intersites viable sans aucun contournement.
Le lab PortSwigger 'SameSite Lax bypass via cookie refresh' montre l'exploit : une page force une navigation de premier niveau vers la cible pour rafraîchir le cookie de session, puis immédiatement (< 120 s) déclenche un POST intersite dans une fenêtre popup. Le POST réussit parce que le cookie fraîchement émis est dans sa période de grâce Lax+POST. C'est un détail d'implémentation du navigateur, pas une exigence de spec.