Aller au contenu

Cookies HTTP : pourquoi ne jamais y fonder une décision de droits

Un cookie est un fragment d’état que le serveur demande au navigateur de conserver et de renvoyer à chaque requête. La donnée vit donc côté client, hors de tout contrôle du serveur entre deux échanges. Un utilisateur peut la lire, la modifier et la rejouer à volonté. Fonder une gestion de droits ou un accès à des ressources sur la valeur d’un cookie est de ce fait une erreur de conception.

Le serveur émet un cookie via l’en-tête Set-Cookie, le navigateur le renvoie ensuite dans l’en-tête Cookie :

# Réponse du serveur
Set-Cookie: role=user; Path=/
# Requêtes suivantes du client
Cookie: role=user

Des attributs encadrent son usage : Path et Domain délimitent sa portée, Expires sa durée de vie, HttpOnly le rend inaccessible au JavaScript, Secure le réserve à HTTPS, SameSite limite l’envoi cross-site. Ces attributs protègent contre le vol ou l’envoi involontaire du cookie. Aucun n’empêche son propriétaire légitime de le modifier.

Le défaut consiste à placer une information d’autorisation directement dans un cookie en clair, puis à s’y fier côté serveur.

Cookie: role=user
Cookie: isAdmin=0
Cookie: user_id=1042

Le serveur qui lit role pour décider d’un accès, ou isAdmin pour ouvrir une interface d’administration, délègue sa décision de sécurité à une donnée que le client contrôle entièrement.

L’attaque ne demande aucun outil sophistiqué. La valeur se change dans l’inspecteur du navigateur (onglet Application → Cookies) ou en interceptant la requête dans Burp Suite.

# Cookie d'origine
Cookie: role=user
# Cookie modifié, rejoué tel quel
Cookie: role=admin

Si le serveur accorde des droits d’administration sur la seule foi de role=admin, l’élévation de privilèges est immédiate. Le même raisonnement s’applique à isAdmin=0 passé à 1, ou à un user_id changé pour usurper un autre compte.

Le point à retenir : un cookie peut être altéré comme on le veut, et le serveur ne dispose d’aucun moyen de le savoir si la valeur n’est ni signée ni vérifiée.

Signer ou chiffrer le cookie empêche l’altération silencieuse, comme le fait Flask avec itsdangerous ou un JWT avec sa signature. C’est un prérequis d’intégrité, mais pas une politique d’autorisation.

  • Une signature ne protège que si sa clé est forte. Un secret faible se casse hors ligne, et le cookie redevient forgeable.
  • Même signé, un cookie qui porte un rôle reste une affirmation du client. Le serveur doit confronter cette affirmation à une source de vérité, pas la prendre pour argent comptant.

L’autorisation se calcule côté serveur, à partir de l’identité authentifiée et d’un référentiel de droits maîtrisé (base de données, annuaire). Le cookie sert à identifier une session, jamais à porter la décision.

# Vulnérable : le rôle vient du cookie
if request.cookies.get("role") == "admin":
show_admin_panel()
# Correct : le rôle est lu côté serveur pour l'utilisateur authentifié
user = get_user_from_session(request)
if user.has_role("admin"):
show_admin_panel()

Un cookie de session doit contenir un identifiant aléatoire sans signification, associé côté serveur à l’état réel de la session. Rien d’exploitable ne transite alors par le client.

Set-Cookie: SESSIONID=b7c9f1e0a4d84e2fa1c3; HttpOnly; Secure; SameSite=Strict; Path=/

HttpOnly, Secure et SameSite ne corrigent pas le problème d’autorisation, mais ils limitent le vol du cookie par XSS, son interception en clair et son envoi cross-site. Ils font partie de la configuration minimale d’un cookie de session.

Un cookie est une donnée cliente, modifiable par nature. Y inscrire un rôle ou un droit et s’y fier côté serveur ouvre une élévation de privilèges qui ne demande qu’un clic dans l’inspecteur. Signer le cookie protège son intégrité mais ne remplace pas une vérification : l’autorisation se décide côté serveur, à partir d’une source de confiance, et le cookie se limite à identifier une session par un jeton opaque.