Guide de Sécurité PHP: Les sessions


< PrécédentSuivant >Hôtes partagés (Shared Hosts)Bases de données et SQL

Fixation de session

La sécurité des sessions est un sujet sophistiqué, et il n'est pas surprenant que les sessions constituent fréquemment une cible d'attaque. La plupart des attaques de session impliquent une usurpation d'identité (impersonation), par laquelle l'attaquant tente d'accéder à la session d'un autre utilisateur pour se faire passer pour cet utilisateur.

L'élément d'information le plus décisif pour un attaquant est l'identifiant de session, parce qu'il est indispensable pour toute attaque par usurpation d'identité. Il existe trois méthodes communément utilisées pour obtenir un identifiant de session valide:

  • Prédiction

  • Capture

  • Fixation

La prédiction se rapporte au fait de deviner un identifiant de session valide. Avec le mécanisme natif des sessions PHP, l'identifiant de session est extrêmement aléatoire, et il est très improbable que ceci constitue le point le plus faible de votre implémentation.

Capturer un identifiant de session valide est le type d'attaque de session le plus répandu, et il existe de nombreuses approches pour ce faire. Puisque les identifiants de session sont typiquement propagés via les cookies ou les variables GET, les différentes approches se concentrent sur les attaques contre ces méthodes de transfert. Malgré l'existence de quelques failles concernant les cookies dans les navigateurs, elles étaient principalement le fait d'Internet Explorer. Et les cookies sont légèrement moins exposés que les variables GET. Donc, vous pouvez fournir aux utilisateurs qui acceptent les cookies un mécanisme plus sécurisé, en utilisant un cookie pour propager l'identifiant de session.

La fixation est la méthode la plus simple pour obtenir un identifiant de session valide. Même s'il n'est pas très difficile de se défendre contre cela, vous êtes vulnérables si votre gestion de session ne consiste en rien de plus que session_start().

Pour montrer une fixation de session, je vais utiliser le script suivant, session.php:

<?php

session_start();

if (!isset($_SESSION['visits']))
{
    $_SESSION['visits'] = 1;
}
else
{
    $_SESSION['visits']++;
}

echo $_SESSION['visits'];

?>

A la première visite de cette page, vous devriez voir affiché le nombre 1 à l'écran. Lors des visites suivantes, ce nombre devrait augmenter pour refléter le nombre de fois que vous avez visité la page.

Pour démontrer la fixation de session, assurez-vous d'abord que vous n'avez pas d'identifiant de session existant (effacez peut-être vos cookies). Ensuite, visitez cette page en ajoutant ?PHPSESSID=1234 à l'URL. Ensuite, avec un navigateur complètement différent (ou même un ordinateur complètement différent), visitez à nouveau le même URL, avec ?PHPSESSID=1234 ajouté à la fin. Vous vous apercevrez que vous ne verrez pas affiché le nombre 1 lors de votre première visite, mais que vous continuez plutôt la session initiée précédemment.

Pourquoi cela peut-il être problématique? La plupart des attaques par fixation de session utilisent simplement un lien ou une redirection (au niveau du protocole) pour envoyer l'utilisateur sur un site distant avec un identifiant de session ajouté à l'URL. L'utilisateur ne s'en apercevra probablement pas, puisque le site se comportera exactement comme avant. Puisque l'attaquant a choisi l'identifiant de session, ce dernier est déjà connu, et peut être utilisé pour lancer un attaque d'usurpation d'identité (impersonation), telle qu'un détournement de session (session hijacking).

Il est assez facile d'empêcher une attaque aussi simpliste que celle-ci. S'il n'existe aucune session active associée à l'identifiant de session présenté par l'utilisateur, alors regénérez-le, juste pour être certain.

<?php

session_start();

if (!isset($_SESSION['initiated']))
{
    session_regenerate_id();
    $_SESSION['initiated'] = true;
}

?>

Le problème d'une défense si simpliste est qu'un attaquant peut simplement initier une session pour un identifiant de session particulier, et ensuite utiliser cet identifiant pour lancer l'attaque.

Pour vous protéger contre ce type d'attaque, considérer d'abord que le détournement de session n'est véritablement utile qu'une fois que l'utilisateur s'est logué ou a obtenu un niveau plus élevé de privilèges. Ainsi, si l'on modifie l'approche, pour regénérer un identifiant de session chaque fois que le niveau de privilège change (par exemple, après avoir vérifié le nom d'utilisateur et le mot de passe), nous aurons pratiquement éliminé le risque d'une attaque réussie par fixation de session.

Détournement de session

Le détournement de session, que l'on peut soutenir comme la plus répandue des attaques de session, se rapporte à toutes les attaques visant à accéder à la session d'un autre utilisateur.

Comme pour la fixation de session, vous êtes vulnérable si votre gestion de session consiste simplement en session_start(), bien qu'exploiter cela ne soit pas aussi simple.

Plutôt que de se concentrer sur la manière d'empêcher que l'identifiant de session ne soit capturé, je vais me concentrer sur la manière de rendre une telle capture moins problématique. Le but est de compliquer l'usurpation d'identité (impersonation), puisque chaque complication augmente le niveau de sécurité. A cet effet, nous allons examiner les étapes nécessaires à la réussite d'un détournement de session. Dans chaque scénario, nous supposerons que l'identifiant de session a été compromis.

Avec une gestion de session des plus simplistes, la réussite d'un détournement de session ne nécessite qu'un identifiant de session valide. Pour améliorer cela, nous devons savoir s'il existe quelque chose de plus dans la requête HTTP que nous pourrions utiliser pour une identification supplémentaire.

Note

Il n'est pas raisonnable de se fier à quoi que ce soit au niveau TCP/IP, par exemple l'adresse IP, car ce sont des protocoles de niveau plus bas, non destinés à satisfaire des activités se déroulant au niveau de HTTP. Un seul utilisateur est susceptible d'avoir une IP différente à chaque requête, et plusieurs utilisateurs pourraient avoir la même adresse IP.

Rappelez-vous une requête HTTP typique:

GET / HTTP/1.1
Host: example.org
User-Agent: Mozilla/5.0 Gecko
Accept: text/xml, image/png, image/jpeg, image/gif, */*
Cookie: PHPSESSID=1234

Seul l'en-tête Host est requis par HTTP/1.1. Il semble donc déraisonnable de se fier à quoi que ce soit d'autre. Cependant, la cohérence est vraiment tout ce dont nous avons besoin, puisque nous sommes seulement intéressés à compliquer l'usurpation d'identité, sans affecter défavorablement les utilisateurs légitimes.

Imaginez que la requête précédente soit suivie par une requête avec un User-Agent différent :

GET / HTTP/1.1
Host: example.org
User-Agent: Mozilla Compatible (MSIE)
Accept: text/xml, image/png, image/jpeg, image/gif, */*
Cookie: PHPSESSID=1234

Même si le même cookie est présenté, devrait-on supposer qu'il s'agit du même utilisateur? Il semble très improbable qu'un navigateur change son en-tête User-Agent entre deux requêtes. Correct, non? Modifions la gestion de session pour effectuer une vérification supplémentaire:

<?php

session_start();

if (isset($_SESSION['HTTP_USER_AGENT']))
{
    if ($_SESSION['HTTP_USER_AGENT'] != md5($_SERVER['HTTP_USER_AGENT']))
    {
        /* Prompt for password */
        exit;
    }
}
else
{
    $_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']);
}

?>

Désormais, l'attaquant doit non seulement présenter un identifiant de session valide, mais aussi l'en-tête User-Agent correct, associé à la session. Ceci complique légèrement les choses, et est par conséquent un peu plus sécurisé.

Pourrait-on améliorer cela? Considérez que la méthode la plus répandue pour obtenir les valeurs d'un cookie est d'exploiter une vulnérabilité d'un navigateur comme Internet Explorer. Ces exploits impliquent que la victime visite un site de l'attaquant, et donc l'attaquant sera capable d'obtenir l'en-tête User-Agent correct.

Imaginez que nous forcions l'utilisateur à passer le hash MD5 du User-Agent à chaque requête. L'attaquant ne pourrait plus simplement recréer les en-têtes que contiennent les requêtes de la victime, mais il serait également nécessaire de passer cette petite information supplémentaire. Bien qu'il ne soit pas trop difficile de deviner la construction de cet élément (token) particulier, nous pouvons compliquer ce travail d'estimation en ajoutant simplement un petit peu de hasard à la manière dont nous construisons l'élément (token):

<?php

$string = $_SERVER['HTTP_USER_AGENT'];
$string .= 'SHIFLETT';

/* Add any other data that is consistent */

$fingerprint = md5($string);

?>

En gardant à l'esprit que nous passons l'identifiant de session dans un cookie, ce qui requiert déjà qu'une attaque ait été utilisée pour compromettre ce cookie (et vraisemblablement toutes les en-têtes HTTP également), nous devrions passer cette empreinte (fingerprint) dans une variable d'URL. Celle-ci devrait se trouver dans toutes les URLs, comme s'il s'agissait de l'identifiant de session, car les deux devraient être requis pour que la session se poursuive automatiquement (en plus de réussir toutes les vérifications).

Pour s'assurer que les utilisateurs légitimes ne soient pas traités comme des criminels, demandez simplement un mot de passe si une vérification échoue. Si une erreur dans votre mécanisme suspecte erronément un utilisateur d'une attaque par usurpation d'identité, demander un mot de passe avant de poursuivre est la manière la moins offensante de gérer la situation. En réalité, vos utilisateurs apprécieront peut-être la petite protection supplémentaire mise en évidence par une telle requête.

Il existe de nombreuses méthodes différentes que vous pouvez utiliser pour compliquer l'usurpation d'identité et protéger vos applications des détournements de session. J'espère que vous ferez au-moins quelque chose de plus que session_start(), et que vous serez capables d'émettre vos propres idées. Rappelez-vous simplement de rendre les choses difficiles aux méchants (bad guys), mais simples pour les gentils (good guys).

Note

Certains experts affirment que l'en-tête User-Agent n'est pas assez cohérent pour être utilisé de la manière décrite. L'argument est qu'un proxy HTTP dans un cluster peut modifier l'en-tête User-Agent de manière non cohérente avec les autres proxies du même cluster. Bien que je n'aie jamais observé cela moi-même (et sois à l'aise en me fiant à la cohérence de User-Agent), c'est quelque chose que vous pourriez vouloir envisager.

L'en-tête Accept a été connu pour changer de requête en requête dans Internet Explorer (selon que l'utilisateur rafraîchit ou non son navigateur). Ainsi, on ne devrait pas compter dessus pour la cohérence.


< PrécédentSuivant >Hôtes partagés (Shared Hosts)Bases de données et SQL