Mesures contre la fixation de session (session fixation)

Principe de base OWASP : l'identifiant de session doit être renouvelé à chaque changement de niveau de privilège (connexion, élévation de droits, passage anonyme authentifié), et l'ancienne session doit être détruite côté serveur.

Régénération de l'ID de session à l'authentification — mesure principale

Ne pas se contenter de vider les données : il faut un nouvel identifiant et l'invalidation de l'ancien enregistrement serveur (sinon l'ID pré-positionné par l'attaquant reste valide).

Plateforme API correcte

PHP session_regenerate_id(true) (le true supprime l'ancien fichier de session)
Java Servlet 3.1+ request.changeSessionId() ou session.invalidate() puis request.getSession(true)
express-session (Node) req.session.regenerate(cb) puis réécriture des données
Django automatique : django.contrib.auth.login() appelle request.session.cycle_key()
ASP.NET Core pas de rotation intégrée : abandonner la session et forcer un nouveau cookie
Flask sessions signées côté client par défaut (pas d'ID serveur) ; avec Flask-Session / stockage serveur, la rotation du sid doit être explicite

session_start();
// après validation des identifiants uniquement
session_regenerate_id(true);
$_SESSION['authentifie'] = true;

𝑆
𝐸
𝑆
𝑆
𝐼
𝑂
𝑁
[
′
𝑢
𝑠
𝑒
𝑟
𝑖
𝑑
′
]
=
S
	​

ESSION[
′
user
i
	​

d
′
]=userId;

// express-session : régénération correcte
req.session.regenerate(err => {
if (err) return next(err);
req.session.userId = user.id;
req.session.save(() => res.redirect('/dashboard'));
});

Point de vigilance Flask : session.clear() n'est pas une régénération d'ID. Avec la session cookie par défaut, il n'existe pas d'ID côté serveur (donc pas de fixation d'ID classique, mais un risque de réutilisation du cookie signé) ; avec un backend serveur, il faut supprimer l'entrée de stockage et générer un nouveau sid. L'API publique est session.permanent = True, pas session._permanent.

Gestion « stricte » des sessions (souvent oubliée)

Refuser tout ID de session non généré par le serveur : si l'ID reçu est inconnu du stockage, ne pas le réutiliser — créer un ID neuf (interdire le mode « permissif » qui accepte n'importe quelle valeur).
Jamais d'ID de session dans l'URL ni accepté depuis un paramètre GET/POST : en PHP, session.use_only_cookies=1, session.use_trans_sid=0.
Pas de fixation via en-tête ou balise : ignorer toute tentative de définition du cookie de session côté client.

Cookies de session durcis

Secure (HTTPS uniquement), HttpOnly (inaccessible en JS), SameSite=Lax ou Strict.
Préfixe __Host- (impose Secure, Path=/, interdit Domain) : bloque la fixation par un sous-domaine compromis, vecteur classique de session fixation.
Pas d'attribut Domain trop large, pas de partage de cookie entre sous-domaines non maîtrisés.

session_set_cookie_params([
'lifetime' => 0, // cookie de session
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict',
]);
session_name('__Host-SID');

Génération et stockage des identifiants

ID d'au moins 128 bits d'entropie, issu d'un CSPRNG (secrets.token_urlsafe(32), crypto.randomBytes(32)).
Nom de cookie non révélateur de la technologie (SID plutôt que PHPSESSID/JSESSIONID).
Données sensibles côté serveur (Redis, base) ; le cookie ne porte que l'ID opaque.

SET "session:" '{"user_id":42,"created_at":1712345678}'
EXPIRE "session:" 3600

Expiration et invalidation

Timeout d'inactivité court (15–30 min pour un contexte sensible) et timeout absolu (ex. 8 h), tous deux appliqués côté serveur, pas seulement via maxAge.
Déconnexion : destruction de l'entrée serveur et suppression du cookie.
Possibilité d'invalider toutes les sessions d'un compte (changement de mot de passe, compromission).

Contrôles contextuels — avec précautions

Lier la session à l'IP ou au User-Agent détecte certains détournements, mais :
l'IP varie légitimement (mobile, CGNAT, proxys) forte proportion de faux positifs ; préférer un contrôle sur le préfixe /24 ou l'ASN, ou un simple signalement plutôt qu'une invalidation systématique ;
le User-Agent est trivialement rejouable par l'attaquant valeur défensive faible, utile surtout en journalisation.

Ces contrôles sont complémentaires, jamais un substitut à la régénération.

Protection CSRF associée

Token synchronizer par session (renouvelé lors de la rotation d'ID) ou double-submit cookie, en plus de SameSite.
Note : le module Node csurf est déprécié — utiliser csrf-csrf ou une implémentation maintenue. Django fournit {% csrf_token %}.

Journalisation et détection

Tracer : création/rotation/destruction de session, échecs d'authentification, changements de contexte en cours de session.

{
"event": "session_rotated",
"old_session_id_hash": "sha256:…",
"new_session_id_hash": "sha256:…",
"user_id": 42,
"ip": "192.0.2.1",
"timestamp": "2024-04-06T12:34:56Z"
}

Ne jamais journaliser un ID de session en clair : stocker un hachage.

Test de validation
Obtenir un ID de session anonyme S1 et le forcer dans le navigateur de la victime.
La victime s'authentifie.
Vérifier que l'ID a changé (S2 ≠ S1) et que S1 rejoué par l'attaquant donne un accès non authentifié (entrée serveur supprimée).
Rejouer le test avec un ID arbitraire jamais émis par le serveur : il doit être refusé.

Risques résiduels

Vol de cookie post-authentification (XSS, MITM) : la rotation n'y répond pas CSP, HttpOnly, HSTS, TLS 1.2+ minimum, réauthentification pour les actions sensibles.
Sous-domaine compromis : sans __Host-, un sous-domaine peut écraser le cookie de session.
« Se souvenir de moi » : jeton persistant distinct de la session, à usage unique et rotatif, révocable.

Checklist

Mesure État Vérification

Rotation d'ID à la connexion + destruction serveur de l'ancienne ID différent avant/après ; ancien ID inopérant
Refus des ID non générés par le serveur Injection d'un ID arbitraire
Aucun ID de session dans l'URL Config use_only_cookies, revue des liens
Secure, HttpOnly, SameSite, __Host- Inspection des en-têtes Set-Cookie
Entropie ≥ 128 bits via CSPRNG Revue de code / analyse statistique
Timeouts inactivité + absolu côté serveur Test d'expiration
Invalidation à la déconnexion et au changement de mot de passe Rejeu du cookie après logout
Protection CSRF, token renouvelé à la rotation Requête sans