Outils pour utilisateurs

Outils du site


php:session

Gestion de session solide

Beaucoup de gestions de sessions en php me semblent mal faites et dangereuses. Voici un exemple de gestion de session créé par mes soins et qui me semble bien plus sûre (Si vous voyez des failles, n'hésitez pas à m'en faire part). Buts: Simplicité, solidité.

Dans cet exemple, le login/mot de passe est codé en dur (toto/titi). A vous d'adapter pour taper dans LDAP, une base mySQL ou autre.

Caractéristiques:

  • On ne fait aucune confiance aux données client (pas même à la date d'expiration du cookie de session). Tout est stocké côté serveur ($_SESSION).
  • Toute page devant être protégée doit juste inclure include 'session.inc'; check_login();. Tout accès non autorisé redirige automatiquement vers la page de login.
  • La session expire automatiquement en cas d'inactivité de l'utilisateur (La date de dernière activité est automatiquement mise à jour à chaque affichage de page).
  • Protection contre le vol de cookie de session (session hijacking tel que Firesheep): A chaque appel de page, on vérifie que l'adresse IP du client n'a pas changée. Le cas de multiples utilisateurs derrière le même proxy (même IP) est pris en compte.
  • Une clé secrète unique aléatoire est générée côté serveur (et jamais envoyée). Elle peut servir pour signer les formulaires (HMAC) ou générer des token de formulaires (protection contre XSRF). Voir $_SESSION['uid'].
  • L'utilisation de phpsessionID dans l'URL est bloquée.

Améliorations possibles:

  • Ajouter une captcha pour empêcher les attaques brute force.
  • Contrôler que le User-Agent n'a pas changé.
  • En cas d'accès non autorisé à une page, rediriger vers cette page après le login.
  • Améliorer la sécurité du cookie de session:
    • Limiter le cookie à un chemin précis et/ou à un sous-domaine précis.
    • Si vous êtes en SSL, passer le cookie en mode “Secure” pour qu'il ne soit plus transmis en HTTP.
  • Supprimer le cookie de session côté client pour faire plus propre (même si du point de vue sécurité cela n'a aucune importance étant donné que cette session est expirée coté serveur et donc inutilisable).
  • Effectuer régulièrement des changements de cookie de session ? (Voir même à chaque requête). (Est-ce que cela rend le piratage plus complexe ?)

La page de login:

login.php
<?php 
include 'session.inc';
if (isset($_POST['login']) && isset($_POST['password']) && (check_auth($_POST['login'], $_POST['password'])))
{
	header('Location: index.php');
}
?>
<html>
<head><title>Login</title></head>
<body>
<form method="post" action="login.php">
Please login:<br>
        Login : <input type="text" name="login"> <br>
        Password : <input type="password" name="password">
        <input type="submit" value="Login">
</form>
</body>
</html>

La page de logout:

logout.php
<?php include 'session.inc'; logout(); ?>

Une page-type accessible uniquement si l'utilisateur est logué:

index.php
<?php include 'session.inc'; check_login(); ?>
<html>
<head><title>Index</title></head>
<body>
Hello <?php print $_SESSION['username']; ?>, you are logged in.<br>
<a href="logout.php">Logout</a>
</body>
</html>

Et enfin la gestion de session elle-même:

session.inc
<?php
/* session.inc 
Session management.
 
Instructions: Each page must include: include 'session.inc'; check_login();
 
Features:
- If the user is not authenticated, he/she will be automatically redirected to login page.
- Everything is stored on server-side (we do not trust client-side data, such as cookie expiration)
- IP addresses are checked on each access to prevent session cookie hijacking (such as Firesheep)
- Session expires on user inactivity (Session expiration date is automatically updated everytime the user accesses a page.)
- A unique secret key is generated on server-side for this session (and never sent over the wire) which can be used
  to sign forms (HMAC) or generate form tokens (to prevent XSRF attacks). (See $_SESSION['uid'] )
 
*/
 
define('INACTIVITY_TIMEOUT',3600); // (in seconds). If the user does not access any page within this time, his/her session is considered expired.
 
ini_set('session.use_cookies', 1);       // Use cookies to store session.
ini_set('session.use_only_cookies', 1);  // Force cookies for session (phpsessionID forbidden in URL)
ini_set('session.use_trans_sid', false); // Prevent php to use sessionID in URL if cookies are disabled.
 
session_start();
 
function allIPs()
// Returns the IP address of the client (Used to prevent session cookie hijacking.)
{
    $ip = $_SERVER["REMOTE_ADDR"];
    // Then we use more HTTP headers to prevent session hijacking from users behind the same proxy.
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip=$ip.'_'.$_SERVER['HTTP_X_FORWARDED_FOR']; }
    if (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip=$ip.'_'.$_SERVER['HTTP_CLIENT_IP']; }
    return $ip;
}
 
function check_auth($login,$password) 
// Check that user/password is correct.
{
    if ($login=="toto" && $password=="titi")
    {
        $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // generate unique random number (different than phpsessionid)
                                                                // which can be used to hmac forms and form token (to prevent XSRF)
        $_SESSION['ip']=allIPs();                // We store IP address(es) of the client to make sure session is not hijacked.
        $_SESSION['username']=$login;
        $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT;  // Set session expiration.
        return True;
    }
    else
    {
        return False;
    }
}
 
function check_login()
// Make sure user is logged in. Redirect to login page if not.
{
    // If session does not exist on server side, or IP address has changed, or session has expired, show login screen.
    if (!isset ($_SESSION['uid']) || !$_SESSION['uid'] || $_SESSION['ip']!=allIPs() || time()>=$_SESSION['expires_on'])
    {
        logout();
    }
    $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT;  // User accessed a page : Update his/her session expiration date.
}
 
function logout()
// Force logout, redirect to login page.
{
    unset($_SESSION['uid'],$_SESSION['ip'],$_SESSION['expires_on']);   // Delete server-side session info
    header('Location: login.php');
    exit();
    // We do not bother deleting the phpsessionID cookie, because it can't be used anyway (All server-side
    // data attached to this session cookie is deleted. This makes the cookie useless. )
}
 
?>

Si vous avez des remarques, idées, critiques:

Discussion

le hollandais volant, 2011/02/07 18:37

“Tout accès non autorisé redirige automatiquement vers la page de login.”

J'utiliserais la redirection vers la page “logout” : ça détruit les variables de sessions coté serveur, puis redirige sur le “login”. Même si c'est pas obligatoire, ça détruira quelques variables de session au cas où elles avaient été créées.

Autrement, j'ai moi même quelques soucis avec les $_SERVER[“REMOTE_ADDR”], $_SERVER['HTTP_X_FORWARDED_FOR'] et $_SERVER['HTTP_CLIENT_IP'] quand je passe par un proxy pour accéder à mon site, ça doit venir de moi mais juste pour prévenir…

Sébastien SAUVAGE, 2011/02/08 10:32

En fait, les variables de session sont détruites lors de la redirection vers la page de login, donc pas de soucis.

Pour le coup du REMOTE_ADDR, j'ai aussi eu des soucis dans certaines entreprise dont les requêtes sortent aléatoirement par des serveurs différents. Ça peut effectivement éventuellement poser problème. Je ne sais pas si le user-agent serait plus intéressant pour limiter le risque de vol de session (ou bien une autre donnée ?).

John, 2011/02/08 15:14

Peut-être utiliser un hash de la combinaison (IP+useragent) ? J'ai parfois l'impression que REMOTE_ADDR n'en fait qu'à sa tête…

Sébastien SAUVAGE, 2011/02/08 17:36

Le User-Agent n'est pas censé changer entre chaque requête, ça pourrait aider (il faudra sans doute un peu de temps au hacker pour comprendre qu'il se fait jeter à cause du fait que son user-agent n'est pas le même que celui du client).

Par contre, comment tu chopes l'adresse IP si ce n'est pas avec REMOTE_ADDR ?

John, 2011/02/09 14:31
il faudra sans doute un peu de temps au hacker pour comprendre qu'il se fait jeter à cause du fait que son user-agent

True ! C'est le genre de chose qui saute aux yeux au bout d'un long temps de réflexion “mais pourquoi ça marche pô…”

Par contre, comment tu chopes l'adresse IP si ce n'est pas avec REMOTE_ADDR ?

Bah je ne connais pas d'autre moyen… Aah, j'ai compris ! Ma phrase “J'ai parfois l'impression que REMOTE_ADDR n'en fait qu'à sa tête” n'est pas une mise à l'écart de REMOTE_ADDR :) Mais c'est une bonne question. Un moyen “exotique” rajouterai de la difficulté à appréhender le tout… Je doute qu'il y ai une commande alternative, tu penses donc peut-être à un moyen détourné puis prendre la valeur sortie… Mais on s'éloigne de la simplicité.

Kevin, 2011/03/20 19:28

il faudra sans doute un peu de temps au hacker pour comprendre qu'il se fait jeter à cause du fait que son user-agent

False :) en sécurité, il faut toujours considérer que l'attaquant a le code source du système ! (surtout si c'est open-source!)

Tof, 2012/04/17 10:42

J'ai eu un soucis avec $_SERVER['HTTP_X_FORWARDED_FOR'] qui ajoute 127.0.0.1 à la suite de l'ip de la machine en fonction de l'url. Après plusieurs tests je me suis rendu compte que cela ne se produisait que lorsque l'adresse était directe.

Par exemple avec shaarli sur free à l'adresse http://toto.free.fr/shaarliHTTP_X_FORWARDED_FOR retourne ip, 127.0.0.1 alors qu'avec l'adresse qui contient un '?' http://toto.free.fr/shaarli?do=loginHTTP_X_FORWARDED_FOR retourne seulement ip

Du coup, quand j'essaie de m'authentifier à partir de http://toto.free.fr/shaarli?do=login HTTP_X_FORWARDED_FOR stocke ip et quand cela me redirige vers http://toto.free.fr/shaarli HTTP_X_FORWARDED_FOR retourne ip, 127.0.0.1 et cela me déconnecte car différent de ce qu'il y a dans la variable de sessions ip.

Il est possible de contourner le problème en cliquant sur login à partir de http://toto.free.fr/shaarli? ce qui redirige vers cette même adresse et valide la comparaison avec allIPs et ne me déconnecte pas. Il faut avouer que c'est quand même bizarre. Est-ce que cela est uniquement dû aux paramètres du proxy ? Est-ce que l'utilisation du '?' modifie l'appel du proxy ?

J'espère que cette expérience aidera à améliorer l'utilisation de allIPs()

Le masqueur masqué, 2012/04/17 11:26

La variable HTTP_X_FORWARDED_FOR dépend du type de proxy utilisé et de la politique de ce proxy. On ne le répétera jamais assez, mais un proxy mal configuré est une plaie pour la simple navigation, si ce n'est même un danger pour l'Internet lui-même.

Il est donc fortement probable que c'est le proxy utilisé qui déconne.

Si c'est du côté de l'hébergeur (et il y en a “forcément” un du côté de chez Free), t'es marron. Ce n'est pas la première fois que Free démontrera son incapacité à être un bon hébergeur gratuit.

Tof, 2012/04/17 12:31

Free ne semble pas être en cause car cela marche très bien quand je ne suis pas derrière un proxy. Ce serait bizarre que ce soit l'utilisation d'un proxy qui pose problème du côté de free, d'autant plus que j'ai le même problème avec OVH. Je pense plutôt que c'est la configuration du proxy qui pose problème, mais je n'ai bien sûr aucun accès aux paramètres pour vérifier.

Ceci étant, ne peut-on pas limiter le problème en remplaçant $ip=$ip.'_'.$_SERVER['HTTP_X_FORWARDED_FOR']; par $proxy=explode(”,”,$_SERVER['HTTP_X_FORWARDED_FOR']); $ip=$ip.'_'.$proxy[0];

J'ai vu que certaines personnes contournaient le problème comme ça, mais je ne sais pas si cela perd complètement l'utilité contre le session hijacking ?

Une autre solution est de n'utiliser que du POST et pas de GET, mais ça complique l'implémentation du code html.

Le masqueur masqué, 2012/04/17 12:44

Oops, je n'avais pas compris le problème dans ce sens, toutes mes confuses. Et mes excuses à Free ^^

Oui, c'est bien la configuration de votre proxy qui pose problème.

Faire du POST, c'est une bonne idée générale, sauf pour transmettre des URL à un tiers. (“implémentation du code html” ?)

Votre solution pour contourner ce problème ne me semble pas fiable. On n'est d'abord pas sûr de récupérer la même adresse dans $proxy[0] si on rencontre votre problème. La “bonne” solution serait de bien configurer le proxy, évidemment. Ou faire en sorte que chaque adresse appelée ait une query string, même vide… Ça me semble plus secure. À tester.

Sébastien SAUVAGE, 2011/02/09 15:43

Bon en tous cas l'idée d'ajouter le user-agent est bonne. J'ajouterai ça.

Sébastien SAUVAGE, 2011/02/09 15:47

Et qu'est-ce que vous pensez du fait de changer le cookie de session à chaque appel à une page ?

Le temps que le hacker injecte son cookie volé dans son navigateur, celui-ci devient invalide si l'utilisateur légale a appelé une autre page.

Un cookie volé ne serait donc valide que jusqu'à ce que l'utilisateur légale appelle une autre page, ce qui devrait encore réduire le risque de vol de session. J'ai bon ?

JeromeJ, 2011/02/17 02:08

Ça semble logique en tout cas. Je ne vois pas trop quel pourrait être le problème.

Peut-être que si la transaction est sniffée, le hacker pourrait plus facilement récupérer le cookie au lieu d'aller la cherchée là où elle est. Bon, sur ce point, je laisse un pro en juger. Je ne suis ni un sniffer ni un hacker :-D

Arnaud, 2011/08/04 14:32

La modification du cookie à chaque appel de page peut poser des problèmes en cas de perte de connexion. En effet, si la réponse du serveur, contenant le nouveau cookie, se perd dans les méandres d'internet avant d'arriver au client, le client ne pourra mettre à jour son cookie et se fera alors jeter par le serveur. La reconnexion sera alors nécessaire pour accéder à nouveau au site.

Sébastien SAUVAGE, 2011/08/19 16:05

Pas faux.

Le masqueur masqué, 2011/09/14 18:16

Ce risque peut être mitigé par le principe de “cookie glissant”, je pense, plus lourd à mettre en place cependant.

Le cookie 1 est valable pour 5 requêtes. Au bout de la 4ème requête, le cookie2 est envoyé. Si le cookie1 est réutilisé une fois encore, destruction côté serveur du cookie2 et envoi du cookie3 avec invalidation du cookie1. Une autre anomalie dans les x minutes, et tous les cookies sont invalidés ⇒ reconnexion et pourquoi pas affichage d'un message “connexion de mauvaise qualité ou hacking. Êtes-vous sur un réseau wifi public ? Voulez-vous passer en https ?”.

Vous me direz que le cookie1 peut être utilisé par le hacker. Évidemment. Mais ceci dit, l'utilisateur légal envoie aussi des cookies, qui viennent perturber le système et déclencher l'éjection.

Le truc c'est que ce système fonctionne très bien tant qu'il n'y a pas de problème, et tolère une perte de paquet occasionnelle. Elle permet aussi de résister à un hacker qui voit le changement périodique de cookie : il est alors forcé de “supprimer” l'utilisateur légal si il veut garder la connexion. On peut réduire à 3 requêtes la validité d'un cookie et dans ce cas, le hacker est obligé d'injecter continuellement les cookies qu'il intercepte, et il a intérêt à bien s'intercaler entre l'utilisateur légitime et le hacker. Pour les utilisateurs à la connexion moisie, on peut aussi proposer de désactiver ce système “glissant”, mais avec affichage d'un bandeau (ou tout autre moyen hein) signifiant que la protection n'est pas optimum et que si il n'est pas à l'origine de ce choix, qu'il se déconnecte immédiatement (comme ça, si c'est le hacker qui désactive le système, piouf !) (d'ailleurs, ce système devrait être désactivable sans avoir à redonner son mot de passe, afin que, si il y a “intrusion”, il ne soit pas volé).

Je pense que pour un site “classique”, ceci assure une sécurité un peu plus accrue qu'un simple cookie, sans perdre d'un coup la connexion à cause d'un paquet perdu avec le principe du cookie jetable. Et la contre-mesure est forcément “home-made” pour le moment, ce qui n'est pas une protection en soi ni pérenne, mais qui nécessite tout de même de la part du hacker un investissement important et pas à la portée du premier venu. Pour un site de taille moyenne soucieux de la sécurité, ça me semble un bon compromis.

Qu'en pensez-vous ?

Le masqueur masqué, 2011/09/14 18:19

Ha et j'oubliais. Si le hacker prend la priorité de l'utilisateur légal dans les cookies, ce dernier se fait éjecter, puisque mauvaise utilisation de cookie. Devant le formulaire de login, il éjecte du coup le hacker car le login est prioritaire dans la base de données (il écrase tout système de cookie glissant déjà en place). On peut du coup mettre en place un système qui compte les relogins dans un laps de temps défini. 3 reconnexions ⇒ “problème de connexion, de hacking, blablabla”…

Arnaudm, 2011/11/08 09:00

Bonjour à tous, je m'interesse aux gestions de session en ce moment et je vois plusieurs choses sur le net et j'aimerais avoir votre avis..

sur ce tuto : http://www.siteduzero.com/tutoriel-3-69660-les-bases-du-systeme.html On voit une fonction actualiser_session() qui est équivalente à la fonction check_login() de sebsauvage

seulement, en plus de vérifier qu'une variable de session user existe, ils refont la verification en base du mot de passe… est ce que tout ceci est nécessaire ?

Le masqueur masqué, 2011/11/08 09:58

Perso, je suis contre le stockage où que ce soit du mot de passe (sauf en base, évidemment, et au moins hashé).

Quitte à vérifier que l'utilisateur est bien le même et qu'il évolue dans le même contexte, je préfère faire un hash de tout le contexte (tu concatènes le login, le user-agent, l'IP, bref, toutes les infos que tu peux, et tu hash le tout) et le comparer au hash de contexte stocké en session, voire en base à chaque nouvelle session afin de certifier une connexion depuis un même contexte.

Il faut garder à l'esprit que le couple login/mot de passe est souvent le seul moyen d'identifier un utilisateur. Le stocker et le balader un peu n'importe où, c'est prendre des risques supplémentaires.

Avec le hash de contexte, en cas de vol de session il faut que le hacker reproduise exactement le même contexte que l'utilisateur (enfin, ou en donne l'illusion), sans savoir quels paramètres sont pris en compte. Alors, effectivement, c'est simple à faire, mais avec un système d'alerte, l'utilisateur légitime peut être informé et agir.

Arnaudm, 2011/11/09 14:47

Ok merci, je m'en vais prendre la gestion de session sur cette page pour mon projet :-)

Arnaudm, 2011/11/30 12:04

Quelques petites questions :

dans la fonction check_login, pour cette ligne : if (!isset ($_SESSION['uid']) || !$_SESSION['uid'] || $_SESSION['ip']!=allIPs() || time()>=$_SESSION['expires_on'])

A quoi correspond ce test ? !$_SESSION['uid']

!isset ($_SESSION['uid']) correspond à l'existence de cette variable mais avec simpement un ”!” ?

Merci d'avance ;)

Sébastien SAUVAGE, 2011/12/12 16:08

$_SESSION['uid'] est créé côté serveur quand l'utilisateur s'est correctement connecté. Absence de cette donnée côté serveur = session invalide.

ça sert à vérifier que la session existe bien côté serveur (et qu'elle est valide).

Tof, 2012/04/18 18:54

Si cela peut aider d'autres personnes, je me suis servi de ton code pour créer une classe Session ce qui évite l'utilisation de plusieurs fichiers : http://tontof.net/?1334768400

mbmk92, 2014/07/29 01:32

Salut,

vraiment parfait ce code. Il est idéal pour améliorer la page login de l'application que je développe. Cependant, je rencontre un problème et je sollicite vivement votre aide svp.

En faite, au lieux de mettre le login et le mot de passe en dur, je souhaite me connecter à ma base de données pour effectuer la vérification. Mais je sais pas où mettre le code de connexion à la base de données, ainsi que la requête de vérification.

Merci d'avance pour votre aide.

Mathieu, 2014/09/29 23:41

Bonjour,

Le code est vraiment bien fait ! Cependant, je voudrais faire la même chose que @mbmk92avec un match sur un login et un pass hashé dans une base de données, je suis un peu perdu, pouvez-vous m'aider? (Je déterre un peu une ancien topic :/)

Mathieu

Entrer votre commentaire. La syntaxe wiki est autorisée:
   __  ___   ___    _  __   _  __   __  ___
  /  |/  /  / _ \  / |/ /  | |/_/  /  |/  /
 / /|_/ /  / // / /    /  _>  <   / /|_/ / 
/_/  /_/  /____/ /_/|_/  /_/|_|  /_/  /_/
 
php/session.txt · Dernière modification: 2014/07/12 13:26 (modification externe)