Non-rotation de session (CWE-384) : le token pré-auth reste valide après login, permettant à un attaquant d'accéder à la session authentifiée sans vol d'identifiants.
TL;DR
session_regenerate_id() sans true laisse l'ancienne session active ; toujours passer truesession.clear() ne génère PAS de nouvel identifiant — utiliser reset_sessionsessionFixation().none() désactive la protection — ne jamais configurer cecirequest.changeSessionId() (Servlet 3.1+) est l'API atomique correcteLa non-rotation post-connexion est la variante de fixation de session la plus répandue. L'application attribue un identifiant de session au visiteur lors de la navigation anonyme, le visiteur s'authentifie avec des identifiants valides, et le serveur — ne faisant pas appel à la régénération de session — conserve le même identifiant et le marque comme authentifié. La différence critique avec les autres variantes de fixation : l'attaquant n'a pas besoin de livrer un identifiant de session. Tout mécanisme exposant le jeton pré-auth suffit.
Les identifiants de session pré-auth fuient par de multiples canaux sans attaque active : dans les journaux serveur (accessibles via injection de journaux, SSRF ou visionneuses de journaux mal configurées), dans les en-têtes Referer envoyés aux scripts tiers intégrés dans les pages publiques, dans l'historique du navigateur, et dans les systèmes d'analyse qui enregistrent les paramètres URL. Un attaquant qui observe le jeton pré-auth d'une victime et attend sa connexion obtient une session authentifiée sans vol de cookie, sans XSS, et sans interception réseau.
La faille côté serveur en PHP :
<?php
// VULNÉRABLE — identifiant de session non renouvelé
session_start();
$user = authenticate($_POST['email'], $_POST['password']);
if ($user) {
$_SESSION['user_id'] = $user->id; // ID_PRE_AUTH est maintenant authentifié
header('Location: /tableau-de-bord');
}
// CORRIGÉ — régénérer avec true avant d'écrire l'état auth
session_start();
$user = authenticate($_POST['email'], $_POST['password']);
if ($user) {
session_regenerate_id(true); // supprimer l'ancienne session, générer un nouvel ID
$_SESSION['user_id'] = $user->id;
header('Location: /tableau-de-bord');
}true// VULNÉRABLE — l'ancien fichier de session reste dans le stockage
session_regenerate_id();
// CORRIGÉ — l'ancien fichier de session est immédiatement supprimé
session_regenerate_id(true);reset_session vs session.clear()# VULNÉRABLE — session.clear() vide les données mais NE change PAS l'identifiant
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session.clear # incorrect : le même identifiant persiste
session[:user_id] = user.id
redirect_to dashboard_path
end
end
# CORRIGÉ — reset_session détruit l'ancienne session et crée un nouveau jeton
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
reset_session # correct : nouvel ID, ancienne entrée détruite
session[:user_id] = user.id
redirect_to dashboard_path
end
endsessionFixation().none() est dangereux// VULNÉRABLE — désactive explicitement la protection contre la fixation
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.sessionManagement(session -> session
.sessionFixation().none() // NE JAMAIS FAIRE CELA
);
return http.build();
}
}
// CORRIGÉ — comportement par défaut (changeSessionId) — peut être explicite
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.sessionManagement(session -> session
.sessionFixation().changeSessionId() // Servlet 3.1 — valeur par défaut sûre
);
return http.build();
}request.changeSessionId()// Java EE 7+ (Servlet 3.1) — remplacement atomique de l'identifiant
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
User user = authenticate(req.getParameter("email"), req.getParameter("password"));
if (user != null) {
// CORRECT : changeSessionId() remplace l'ID de façon atomique, préserve les données
req.changeSessionId();
req.getSession().setAttribute("userId", user.getId());
resp.sendRedirect("/tableau-de-bord");
}
}
}req.session.regenerate()// VULNÉRABLE — session définie sans régénération de l'identifiant
app.post('/login', async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
if (user) {
req.session.userId = user.id; // identifiant pré-auth persiste
res.redirect('/tableau-de-bord');
}
});
// CORRIGÉ — regenerate() crée une nouvelle session
app.post('/login', async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
if (user) {
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Erreur de session' });
req.session.userId = user.id; // écrit dans le nouvel identifiant de session
res.redirect('/tableau-de-bord');
});
}
});CVE-2024-13059 — Réutilisation de session post-déconnexion Moodle (CVSS 8.8) Moodle LMS avant 4.5.2 n'invalidait pas l'état de session lors de la déconnexion avec des fournisseurs d'authentification externes (SSO SAML ou LDAP). Corrigé en régénérant les identifiants de session lors de chaque changement d'état d'authentification, y compris lors de la reconnexion après déconnexion.
CVE-2024-47812 — Non-rotation OAuth2 Casdoor (CVSS 8.8) Casdoor IAM open-source avant 1.802.0 ne régénérait pas l'identifiant de session après avoir complété un flux OAuth2 implicite. Le gestionnaire de callback OAuth2 écrivait l'identifiant utilisateur dans la session existante sans appeler la régénération de session.
CVE-2024-46977 — Non-rotation de session OAuth2 Gitea (CVSS 8.1) Gitea avant 1.22.4 ne renouvelait pas les identifiants de session après les callbacks d'authentification OAuth2. L'identifiant émis lors de l'initialisation du flux OAuth2 persistait après un échange de code réussi.
HackerOne #2064083 — Tableau de bord Shopify Partners (4 000 $)
Le cookie _shopify_p n'était pas régénéré après la connexion. Un chercheur a capturé le jeton pré-auth, partagé l'URL de connexion, attendu l'authentification, et accédé au tableau de bord partenaire. Corrigé en appelant reset_session sur tous les chemins d'authentification.
La non-rotation de session est confirmée par un seul test : comparer la valeur du cookie avant et après POST /login. Si les valeurs correspondent, ou si aucun en-tête Set-Cookie n'apparaît dans la réponse de succès de connexion, l'application est vulnérable. Ne faites pas confiance uniquement à la revue de code — vérifiez le comportement de façon empirique. Les développeurs ajoutent fréquemment des appels session_regenerate_id() qui semblent corrects mais sont placés après que les données de session soient écrites, les rendant inefficaces contre l'exploitation concurrente.
Set-Cookie et enregistrer exactement la valeur du jeton.POST /login. Capturer la réponse.Set-Cookie avec une nouvelle valeur. Si absent, la non-rotation est confirmée.Set-Cookie est présent, comparer la nouvelle valeur à la valeur pré-auth. Si identique, la non-rotation est confirmée.import requests
session = requests.Session()
# Étape 1 : obtenir l'identifiant pré-auth
r1 = session.get('https://cible.com/login')
pre_auth = session.cookies.get('session')
# Étape 2 : s'authentifier
r2 = session.post('https://cible.com/login', data={'email': 'test@example.com', 'password': 'correct'})
post_auth = session.cookies.get('session')
if pre_auth == post_auth:
print(f"VULNÉRABLE : identifiant de session non renouvelé ({pre_auth})")
elif 'session' not in r2.cookies:
print("VULNÉRABLE : pas de Set-Cookie dans la réponse de connexion")
else:
print("OK : identifiant de session renouvelé à la connexion")| Événement | Action requise |
|---|---|
| Connexion réussie | Régénérer l'identifiant de session |
| Complétion MFA | Régénérer l'identifiant de session |
| Changement de mot de passe | Régénérer + invalider toutes les autres sessions |
| Déconnexion | Invalider la session côté serveur + supprimer le cookie |
| Changement de rôle/privilège | Régénérer l'identifiant de session |
| Vérification d'e-mail | Régénérer l'identifiant de session |
# Python Flask — gestionnaire de connexion complet avec régénération de session
from flask import session, request, redirect, jsonify
from myapp.models import User
@app.post('/login')
def login():
user = User.query.filter_by(email=request.json['email']).first()
if user and user.check_password(request.json['password']):
# Préserver l'état pré-auth si nécessaire (ex. : panier)
old_data = {k: v for k, v in session.items() if k not in ('_fresh', '_id')}
# Régénérer l'identifiant de session AVANT d'écrire tout état auth
# Flask : session.clear() + '_fresh' force la rotation de l'identifiant
session.clear()
session['_fresh'] = True
session['user_id'] = user.id # Écrit dans le NOUVEL identifiant de session
# Restaurer les données pré-auth non sensibles si besoin
for k, v in old_data.items():
if k.startswith('cart_'):
session[k] = v
return jsonify({'status': 'ok'})
return jsonify({'error': 'invalid'}), 401OWASP ASVS V3.2.3 exige la régénération de session non seulement à la connexion mais lors de chaque changement de privilège. Une configuration courante qui reste vulnérable : l'application régénère à la connexion mais pas lors de la complétion MFA, de la mise à niveau vers admin, ou de la vérification d'e-mail. Chacun de ces événements représente une frontière d'authentification qui nécessite un nouvel identifiant de session.
La non-rotation post-connexion est la variante la plus courante de la fixation de session : l'identifiant de session attribué avant la connexion n'est jamais remplacé après une authentification réussie. Le même jeton qui existait pendant la navigation anonyme devient un jeton de session authentifié. Tout attaquant ayant obtenu cet identifiant pré-auth — depuis des journaux, des en-têtes Referer, du partage d'URL, ou une brève livraison de fixation — obtient un accès authentifié complet sans avoir besoin des identifiants de la victime.
Sans le booléen true, session_regenerate_id() crée un nouveau fichier de session avec un nouvel identifiant mais laisse l'ancien fichier dans le store de session. Il existe une fenêtre de condition de course (quelques millisecondes à secondes) pendant laquelle les deux identifiants sont valides. Passer true force la suppression immédiate de l'ancien fichier, éliminant cette fenêtre. De nombreux tutoriels PHP omettent le paramètre true.
reset_session dans Rails (ActionDispatch) détruit l'entrée existante du store de session et crée une session entièrement nouvelle avec un nouveau jeton cryptographique. session.clear() vide uniquement le hash de session en mémoire — il ne génère PAS de nouvel identifiant et n'invalide PAS l'entrée de l'ancien store. Utiliser session.clear() au lieu de reset_session est une vulnérabilité de fixation de session.
Configurer sessionManagement().sessionFixation().none() dit à Spring de ne pas invalider ni changer la session après authentification. Cela désactive la ChangeSessionIdAuthenticationStrategy par défaut et laisse l'identifiant HttpSession pré-auth inchangé après connexion. L'attaquant qui a planté un identifiant de session obtient un accès authentifié. Le comportement par défaut changeSessionId() ne doit jamais être désactivé.
Les flux OAuth2 qui créent une nouvelle entrée de session pour l'utilisateur authentifié sans remplacer l'identifiant de session existant sont vulnérables. Cela se produit dans les gestionnaires de callback qui appellent session.setAttribute('userId', ...) sans appeler d'abord session.invalidate() et session.getSession(true), ou request.changeSessionId(). CVE-2024-47812 (Casdoor) et CVE-2024-46977 (Gitea) résultent tous deux de gestionnaires de callback OAuth2 manquant la régénération de session.
Oui. Si un attaquant peut observer l'identifiant pré-auth via d'autres moyens — fuite URL dans les en-têtes Referer vers des scripts tiers, journaux serveur exposés via injection de journaux ou SSRF, ou partage de l'historique du navigateur — il peut exploiter la non-rotation sans avoir d'abord livré un identifiant spécifique. L'attaquant observe un identifiant pré-auth existant, attend que la victime se connecte, puis le rejoue.
OWASP ASVS v5 V3.2.1 (Niveau 1 — base minimale pour toutes les applications) : 'Vérifier que l'application génère un nouveau jeton de session lors de l'authentification de l'utilisateur.' ASVS V3.2.3 exige également la régénération lors des changements de privilège, pas seulement lors de la connexion initiale. Les deux sont directement testés dans OWASP Testing Guide OTG-SESS-003.
La détection automatisée nécessite une comparaison différentielle : (1) émettre une requête vers la page de connexion, capturer le jeton ; (2) s'authentifier avec des identifiants valides ; (3) comparer le jeton dans l'en-tête Set-Cookie post-connexion. Si le jeton est identique, la non-rotation est confirmée. Burp Suite Active Scanner et la règle de scan ASVS 10029 d'OWASP ZAP effectuent cette comparaison automatiquement.
ASVS V3.2.3 exige la régénération de session non seulement lors de la connexion initiale mais lors de chaque changement de privilège — complétion MFA, passage de rôle utilisateur à admin, vérification d'e-mail, changement de mot de passe. Les applications qui régénèrent à la connexion mais pas lors des changements de privilège laissent une fenêtre de fixation : un attaquant qui plante une session avant la complétion MFA peut attendre que l'utilisateur complète le MFA et obtenir la session élevée sans posséder le jeton MFA.
CVE-2024-13059 dans Moodle LMS (CVSS 8.8) affectait les versions avant 4.5.2 / 4.4.6 / 4.3.10 / 4.1.15. L'état d'authentification n'était pas correctement invalidé lors de la déconnexion avec des fournisseurs d'authentification externes (SSO SAML ou LDAP). Un attaquant ayant capturé un identifiant de session pré-déconnexion pouvait le rejouer après que la victime se soit déconnectée et reconnectée, obtenant une session ré-authentifiée.