vendredi 13 juin 2008

Sécurisation des cookies : une implementation en PHP

Note pour les nouveaux arrivants : vous devriez lire avant tout le précedent article : Introduction à la sécurité des cookies.

Donc... où en étions nous ? Il existe un protocole de sécurisation des cookies (voir le papier de Alex X. Liu pour les details) :

Cookie value =
username|expiration time|(data)k|HMAC(username|expiration time|data|SSL session key, k)
Avec
  • username est le nom de l'utilisateur (ou un identifiant unique)
  • expiration time est la data d'expiration du cookie
  • (data)k est le résultat d'une function de chiffrement par bloc (ex: AES) de data avec la clé k.
  • data est la donnée que vous souhaitez stocker dans le cookie
  • k est le résultat de la fonction HMAC(username|expiration time, sk)
  • sk est une clé secrète que seul le serveur connait
  • SSL session key est l'identifiant de la session SSL en cours.

Ce protocole est sécurisé :

  • Il protège vos cookies contre les "replay attacks" : L'identifiant de la session SSL en cours est unique. Si un utilisateur malicieux essaie d'envoyer le cookie d'un autre dans une autre session SSL, le cookie sera invalide.
  • Il protège la confidentialité des données grâce à une fonction de chiffrement symmétrique : les données sont stockées sous une forme chiffrées.
  • Il protège l'intégrité des données grâce à une fonction HMAC
  • Il protège la clé secrète du serveur contre les "volume attack" : le fait d'inclure le nom de l'utilisateur et la date d'expiration dans la clé k permet d'éviter qu'un analyste malicieux s'amuse à créer un grand nombre de cookies sécurités pour essayer de retrouvé la clé secrète stockée sur le serveur.

Cependant, ce protocole n'est pas adapté à tous les usages :

  1. SSL c'est chouette, ça protège contre les replay attacks, mais :
    • Votre application n'est pas forcément accessible en HTTPS
    • La durée de vie du cookie est limitée à celle de la session SSL.
  2. Le chiffrement des données stockées dans le cookie, c'est cool, mais ça ne vous interresse pas forcement ! Si vous n'en avez pas l'utilité, ça va augmenter la taille du cookie pour rien. Par exemple : si les données sont chiffrées avec en AES256, la valeur chiffrée fera au moins 32 octets, même si les données en clair ne font d'1 octet !

Donc il serait cool d'avoir une classe de gestion des cookies qui puisse être configurable, c'est à dire qui offrirait la possibilité de :

  • activer/désactiver le support SSL
  • activer/désactiver les chiffrement des données à stocker
  • choisir l'algorithme de chiffrement

J'ai créé une classe qui offre toutes ces fonctionnalités. Vous pouvez la télécharger ici.

Utilisation :

Initialisation

include('BigOrNot_CookieManager.php');

$secretKey = 'Cei4Wai4ohcoo3daeHooFiek5Nah3Eet';
$manager = new BigOrNot_CookieManager($secretKey);

Tout ce que vous avez à faire, c'est fournir une clé secrète en premier paramètre. Si vous n'êtes pas inspiré, pour générer la clé vous pouvez utiliser cette commande :

pwgen -sy 65

La configuration par défaut est :

  • Chiffrement des données : activé option name : high_confidentiality (bool)
  • Algorithme de chiffrement : MCRYPT_RIJNDAEL_256 (Rijndael 256) option name : mcrypt_algorithm (voir la doc mcrypt)
  • Mode de chiffrement par bloc : MCRYPT_MODE_CBC (CBC) option name : mcrypt_mode (voir la doc mcrypt)
  • Utilisation de l'identifiant de session SSL : disabled option name : enable_ssl (bool)

Si vous souhaitez modifier la configuration, vous pouvez passer en second paramètre du constructeur un tableau d'options. Par exemple, si vous souhaitez désactiver le chiffrement des données, vous pouvez faire cela :

include('BigOrNot_CookieManager.php');

$secretKey = 'Cei4Wai4ohcoo3daeHooFiek5Nah3Eet';
$config = array('high_confidentiality' => false);
$manager = new BigOrNot_CookieManager($secretKey, $config);

Envoyer un cookie sécurisé

$expire = time() + 86400;
$value = $manager->setCookie('cookieName', 'value', 'username');
Regardez le code source si vous souhaitez plus de détails sur les arguments qu'il est possible d'envoyer en paramètre (globalement, vous pouvez envoyer tous les paramètres que setcookie() supporte).

Lire/vérifier la valeur d'un cookie

$value = $manager->getCookieValue('cookieName');
Si le cookie est invalide (utilisation malicieuse, date d'expiration passée), il est supprimé automatiquement. Si vous n'aimez pas ce comportement, vous pouvez le desactiver en envoyant "false" en 2ème paramètre.

Supprimer un cookie

$manager->deleteCookie('cookieName');

N'hésitez pas à regarder le code source pour plus d'infos, il est commenté. Si vous avez des questions, des remarques, des rapports de bugs, n'hésitez pas à poster un commentaire :)

Note: Il existe une classe qui permet de faire à peu près la même chose avec Django (Python) ici. Cependant elle ne gère ni le chiffrement ni SSL.

Note2: La fonctionnalité SSL ne devrait marcher qu'avec le mod_ssl d'Apache (qui créé généreusement une variable d'environnement SSL_SESSION_ID).

7 commentaires:

Anonyme a dit…

Merci pour cette implémentation, je vais regarder ca de plus près.

juste pour signaler une petite coquille pgwen -sy 65 c'est pwgen, je ne connaissais pas cet outils qui va s'avérer utile

Mat a dit…

C'est corrigé, merci !

yvest a dit…

Bonjour,

Merci pour ces articles intéressants. Je cherche à faire proprement un système - classique - pour que l'utilisateur reste connecté plusieurs jours sur le site

Actuellement, je stocke simplement les informations (username) en session et l'utilisateur est donc connecté le temps de vie de la session PHP.

Je me demande donc quel est la technique la plus "secure" pour une telle fonctionnalité et ce qu'il est possible de stocker dans un cookie

Voici ce que j'envisage :

- l'internaute se connecte en choisissant "rester connecté pendant x jours"
- un cookie sécurisé avec son login est envoyé au client et une session PHP est créé sur le serveur
- quand l'internaute essaye d'accéder à une page sécurisée, on vérifie si sa session PHP est active
- si sa session n'est plus active, on vérifie que le cookie sécurisé contenant username est présent et valide
- si le cookie "username" est présent et valide et que le username existe dans la table des utilisateurs, la session de l'utilisateur est recrée

Penses tu que c'est la bonne manière de procéder ? Ne faut-il pas mieux, à la place du username, utiliser un token qui sera recrée à chaque connexion via le formulaire ou à chaque nouvelle session PHP ?

Merci d'avance pour les idées !

Excellente journée,
Yves

Mat a dit…

Bonjour,

Désolé pour mon temps de réponse !

Cette solution me semble bonne. Ca permet d'avoir la possibilité de te connecter de plusieurs endroits à la fois sur le même compte.

Du coup, je te suggérerais d'intégrer en plus dans le cookie une valeur propre à chaque utilisateur (un simple nombre aléatoire).

Ca permet d'offrir à l'utilisateur la possibilité d'invalider tous les cookies existants pour se connecter à son compte, en régénérant simplement cette valeur.

yvest a dit…

Bonjour,

Je te remercie pour ta réponse.

Je ne comprends pas ce que tu veux dire au niveau de la valeur propre à l'utilisateur ?!

Elle serait créée à quel moment ? Et enregistrée où ? dans le cookie et dans la session ?

Merci,
Yves

Mat a dit…

Voila une version un peu plus détaillée de ce que je voulais dire :

A la création d'un utilisateur sur ton site, tu lui génère une valeur aléatoire (qu'on appellera user_secret) et tu la stock en base de données.


Lorsque l'utilisateur s'authentifie sur ton site, tu créé le cookie et y inclue cette valeur.


Au moment de vérifier la validité du cookie, tu regarde si la valeur user_secret contenue dans le cookie correspond bien a la valeur que tu as en base (tu peux la mettre en cache en session histoire d’éviter des accès base a chaque requête bien entendu).
Si la valeur ne correspond pas, tu invalide le cookie.


Du coup, si tu veux déconnecter toutes les machines connectées au compte d'un utilisateur (s'il suspecte que quelqu'un lui a volé son compte par exemple), il te suffit de changer la valeur user_secret qui est en base.

yvest a dit…

Merci Mat, ça fonctionne nickel. Reste à passer le processus de connexion en https.

++ Yves