La sécurité des session en PHP
Date de publication : 19 juin 2008
Par
Adrien Pellegrini (Espace Web Developpez.com)
Les sessions sont un outil vraiment utile et très utilisé par la
plupart des sites web. Elles permettent de suivre à la trace un
utilisateur en enregistrants certaines informations utiles à garder tout
au long de la navigation sur le site (donc entre les diverses pages). Ces
informations peuvent être par exemple des achats qui sont ajoutés
dans un panier; ou encore un flag servant à stocker les droits d'accès
pour un utilisateur, ... Ces informations doivent donc être sécurisées et ne
doivent être, en aucun cas, altérées ou connue par une personne tierce.
L'aspect sécurité des sessions est souvent oublié. Cet article cherche
donc à vous exposer tous les problèmes liées aux sessions et quelques
résolutions à ces problèmes.
I. Introduction
I.1. Les attaques
I.2. Comment s'en protéger ?
I.3. Requis
II. Les attaques
II.1. Session Exposure
Problème
Solutions
Code
II.2. Session Fixation
Problème
Solutions
Code
II.3. Session Sniffing
Problème
Solutions
Code
II.4. Session Prediction
Problème
Solutions
Code
II.5. Session Hijacking
Problème
Solutions
Code
II.6. Session Poisoning et Session Injection
Problème
Solutions
Code
III. Implémentation des solutions
III.2. Session Exposure
La base de données
L'handler de sessions
open
close
read
write
destroy
gc
III.3. Session Fixation
III.4. Session Sniffing
III.5. Session Prediction
III.6. Session Hijacking
III.7. Session Poisoning et Session Injection
IV. Conclusion
I. Introduction
Cet article va être divisé en deux grandes parties. La première,
comme dit précédemment, va vous expliquer les diverses attaques.
La seconde construira petit à petit une classe sécurisée pour la
gestion des sessions. Ne foncez pas sur cette dernière, ca ne vous
apportera pas grand chose.
I.1. Les attaques
Il existe divers types d'attaques qui permettent de récupérer
l'identifiant de session et de récupérer des informations sensibles
contenues dans les variables de sessions.
Ces différents types d'attaques sont :
1 - Session Exposure
2 - Session Fixation
3 - Session Sniffing
4 - Session Prediction
5 - Session Hijacking
6 - Session Poisoning
7 - Session Injection
Comme vous pouvez le constater nous avons
l'embarra du choix, ce n'est vraiment pas ce qui manque.
I.2. Comment s'en protéger ?
Pour se protéger de ces types d'attaques, il n'y à pas grand
chose à faire. Les problèmes se résolvent assez rapidement avec
des solutions plus ou moins simples.
Pour la plupart des attaques, je vous énoncerais une ou plusieurs
solutions possibles à mettre en oeuvre et je mettrais en
application une ou plusieurs d'entre elles. Une solution protège
généralement de plusieurs attaques ce qui réduit assez bien le code
pour parer à la plupart des éventualités.
Mais ayez toujours à l'esprit
qu'il est impossible de se protéger complètement de tout types
d'attaque. Que certaines solutions sont assez lourdes à mettre
en place car elle allonge le temps d'exécution
de la page. Que d'autres bien qu'elles soient utiles, excluent
certains groupes de webmasters (pas de possibilité de configuration
de leur serveur car ils se trouvent sur un mutualisé) ou excluent
certain groupes d'utilisateurs (ceux qui auraitent désactivés
les cookies).
I.3. Requis
II. Les attaques
II.1. Session Exposure
Problème
Cette vulnérabilité est étroitement liée aux hébergements
mutualisés. Dans ce type d'hébergement, un serveur est
partagé entre plusieurs sites.
Bien que le serveur héberge plusieurs sites, par défaut,
le répertoire où sera stocké les fichiers contenant les
variables de sessions est commmun à tout les utilisateurs (/tmp par
défaut). Ce répertoire est bien evidemment accessible par tout
le monde en écriture pour pouvoir écrire les variables de
sessions. Quand je dis accessible par tout le monde, c'est
plutôt Apache qui donne les droits en écritures aux scripts
PHP. Il n'est alors pas très compliqué, via un petit script
de 15 lignes, de récupérer les informations de sessions.
Normalement le dossier /tmp ne devrait pas être accessible par
un utilisateur via FTP ou autre. Donc si vous y avez accès
vous avez le droit de vous poser quelques questions et d'en poser
à votre administrateur serveur, qui à mal fait son travail.
Un autre problème est un site peu souciant de la sécurité
qui a laisser quelques failles intéressantes ouvertes
(fonction exec, passthru, sheel_exec, system; injection de script; ...).
Solutions
Solution 1 :
Vous faite confiance à votre
administrateur serveur.
Solution 2 :
Vous changer le répertoire de stockage des sessions.
Cette solutions n'est que très rarement envisageable sur
un serveur mutualisé car vous n'avez pas accès au php.ini.
Il faut pour cela changer la directive de configuration
session.save_path. Et même si vous pouviez
changer le répertoire, il faut encore s'assurer que
personne n'y aura accès.
Solution 3 :
Vous sauvegardez vos sessions dans
une base de données.
C'est un peu plus lourd à mettre en oeuvre que les autres
solutions mais c'est aussi la meilleures des protections
pour ce genre d'attaques.
C'est donc cette dernière solution qui va être envisagée ici.
Dans ce cas, même si un des sites hébergé sur le même serveur
que le votre est ouvert à des failles critiques, le votre
restera hors d'atteinte.
Un autre problème survient alors. Si on vous volait
votre login/mot de passe de votre compte pour accéder à la
base de données ou bien que votre site permettrait l'attaque dite
"SQL Injection", un utilisateur malintentionné pourrait aussi voir les
variables de sessions. On pourrait donc crypter les données
dans la base de données bien que la meilleure solution et la plus
économique reste celle d'éliminer les failles d'injection SQL.
Code
II.2. Session Fixation
Problème
Pour qu'une session puisse transiter d'une page à l'autre,
il faut évidemment sauvegarder l'identifiant de session
quelque part. Généralement cet identifiant de session peut
être sauvegardé de deux manières différentes.
Note : L'identifiant de session sera désormais noté SID.
La première consiste à mettre le SID dans
l'URL via une variable GET nommée PHPSESSID par défaut.
Ceci est possible si la directive de configuration
session.use_trans_sid est activée.
Cette directive activée peut poser quelques problèmes
lorsqu'un utilisateurs non attentif, qui est connecté à son
compte sur un tel site, copie-colle bêtement
un lien contenant le SID. Dès lors l'utilisateur qui reçoit
ce lien aura accès au compte comme bon lui semble.
La deuxième solution consiste à sauvegarder le SID dans
un
cookie.
Ceci est possible si la directive de configuration
session.use_cookies est activée.
L'attaque dite "Session Fixation" consiste donc à faire
utiliser à la victime un SID prédéfini par l'attaquant.
Avant de vous montrer un petit exemple pour vous aider à
comprendre, il reste un point à éclaircir. Il est possible de
forcer la génération du SID coté server et interdire tout SID
passé dans l'URL, mais ceci n'est pas disponible sur tout type de
serveur.
Il existe deux grandes catégories de serveurs :
1 - Les serveurs dit "permissif" : c'est à dire des serveurs
qui autorisent un SID quelconque et qui créeront une
nouvelle session avec cet SID.
2 - Les serveurs dit "strict" : c'est à dire des serveurs
qui n'autorisent pas des SID externe, donc qui n'acceptent
que les SID crées par lui-même.
PHP se trouvant dans la première catégorie.
Nous allons créer deux scripts PHP. L'un qui créera une
variable de session et l'autre qui l'affichera.
page1.php |
<?php
error_reporting(E_ALL | E_STRICT);
date_default_timezone_set(' Europe/Brussels ' );
require_once ' Yume/Session.php ' ;
try
{
$ session = Yume_Session: : getSession(Yume_Session: : SESSION_DATABASE);
$session ->setRessourceOptions (array (
' hostname ' = > ' localhost ' ,
' dbname ' = > ' session ' ,
' username ' = > ' root ' ,
' password ' = > ' '
))
- > setCryptOptions(' Bouhhhhhhhh ! ' )
- > start();
} catch (Exception $ e )
{
echo $e ->getMessage ();
}
$session ->test = ' oki ' ;
?>
|
page2.php |
<?php
error_reporting(E_ALL | E_STRICT);
date_default_timezone_set(' Europe/Brussels ' );
require_once ' Yume/Session.php ' ;
try
{
$ session = Yume_Session: : getSession(Yume_Session: : SESSION_DATABASE);
$session ->setRessourceOptions (array (
' hostname ' = > ' localhost ' ,
' dbname ' = > ' session ' ,
' username ' = > ' root ' ,
' password ' = > ' '
))
- > setCryptOptions(' Bouhhhhhhhh ! ' )
- > start();
} catch (Exception $ e )
{
echo $e ->getMessage ();
}
echo $session ->test ;
?>
|
Tout d'abord on va choisir un SID simple pour les test qui
sera 1234. Nous voila donc avec comme URL :
http://localhost:88/dvp/MySession2-fixation/page1.php?PHPSESSID=1234.
Maintenant, soit avec un autre navigateur, soit un autre
ordinateur, vous allez visiter la page2.php avec l'URL :
http://localhost:88/dvp/MySession2-fixation/page2.php?PHPSESSID=1234.
Bien que la session n'est pas censée être démarrée sur la
page2.php, vous constaterez que on obtient bien l'affichage
de "oki".
Solutions
Il existe plusieurs solutions à ce type d'attaque, toutes
ont leurs avantages et inconvénients.
Pour toutes solutions :
session.use_trans_sid doit bien évidemment être à 0
quel que soit la solution adoptée (par défaut cette directive
est désactivée).
Solution 1 :
La solution plus simple consiste sans doute à modifier
deux directives de configuration qui sont :
- session.use_cookies à mettre à 1
- session.use_only_cookies à mettre à 1
Les inconvénients de cette solutions sont que si l'utilisateur
n'accepte pas les cookies, il ne pourra pas démarrer une session
et que vous devez avoir accès au fichier de configuration
php.ini.
Solution 2 :
Une autre solution simple à mettre en place serait de
stocké des informations et vérifier par la suite si elles
ont ou pas changés. Car d'une page à l'autre il est peu
probable qu'une informations choisie change. Mais quel
information choisir ?
On pourrais stocker l'IP de l'utilisateur mais celle-ci
est de loin fiable à 100%. Si l'utilisateur est derrière
un proxy, il changera sans doute d'IP assez régulièrement.
L'IP n'est donc pas à stocker.
Par contre, d'une page à l'autre, le naviguateur ne risque
pas de changer. Nous pourrons donc utiliser cela.
Solution 4 :
Sans aucun doute, la plus sure des solutions est
d'utiliser un serveur sécurisé SSL.
Ici, je ne vais pas utiliser une seule solution mais un
mixte des solutions 2 et 3. La solution 1 ne dépendant pas du
code mais de la configuration de votre serveur, il n'y aura
donc rien de mis en plus pour celle la dans ce code.
Code
II.3. Session Sniffing
Problème
L'attaque "Session Sniffing" consiste à utiliser un
sniffer
pour récupéré l'identifiant de session ou toute autre
information utiles.
Solutions
Il n'y a qu'une solution possible pour se protéger de ce type
d'attaque. C'est de protéger son serveur en utilisant
SSL.
Code
II.4. Session Prediction
Problème
L'attaque "Session Prediction" consiste comme son nom l'indique
à analyser et comprendre la génération de l'identifiant de
session et de pouvoir prédir cet identifiant.
Solutions
La seule solution consiste à modifier quelques directives
de configuration dans le fichier php.ini.
Ces directives sont les suivantes :
session.entropy_length = 0
session.entropy_file =
session.entropy_length = 16
session.entropy_file = /dev/urandom
session.hash_function = 1
session.hash_bits_per_character = 6
|
Il est bien au minimum de généré les identifiants de sessions
avec SHA1 et non plus MD5 (session.hash_function).
Code
II.5. Session Hijacking
Problème
L'attaque "Session Hijacking" n'est rien d'autre que le
résultat d'une autre attaque qui vise à récupéré un identifiant
de session valide.
C'est à dire que l'attaquant a réussi à avoir un SID et
qu'il peut utiliser comme il le veux.
Les attaques possibles pour y arriver sont les suivantes :
- la "Session Fixation"
- la "Session Sniffing"
- la vulnérabilité très connue appellée
Cross-site scripting.
Solutions
Toutes les solutions possibles pouvant être mise en oeuvre
ont déjà été énoncée dans les parties précédantes. En gros
il faut regénéré le SID, utilisé si possible un serveur
sécurisé SSL, propager le SID correctement.
Code
II.6. Session Poisoning et Session Injection
Problème
Ces deux types d'attaques résulte au même problème, les
données de la session se voient altérées.
Un exemple de code à ne pas mettre :
$ _SESSION [ ' login ' ] = $ _REQUEST [ " ' login ' ] ;
$ _SESSION [ ' password ' ] = $ _REQUEST [ ' password ' ] ;
|
On peut mettre directement ce que l'on veut dans la variable
de session via l'URL : http://www.monsite.com/?login=moi&password=1234.
Voici encore un autre exemple mauvais :
$ var = $ _GET [ " something " ] ;
$ _SESSION [ $ var ] = true ;
|
Solutions
Il n'y a pas de solutions concernant la gestion des sessions
ici. Les seules solutions possibles sont de vérifier toutes
les données que vous mettez dans une variable de session.
Il ne faut jamais faire confiance à l'utilisateur.
Il faut éviter aussi d'utiliser la superglobale $_REQUEST,
elle n'apporte rien de bien.
Code
III. Implémentation des solutions
Dans cette partie nous allons créer des classes qui nous permettrons
une gestion des sessions, sécurisée, grâce aux solutions proposées
précédemment.
Ces classes seront construites pour pouvoir gérer les sessions aussi
bien avec une base de données qu'avec le système traditionnel par fichier.
Ne vous étonnez pas donc de voir des bouts de code qui n'ont rien
à voir avec la gestion des sessions en base de données.
Aucun code complet ne sera donné dans cet article. Pour avoir le code
complet et fonctionnel, rendez-vous en fin d'article et téléchargez
la source.
Voici le diagramme des classes que nous auront à la fin. Pas besoin
de l'expliquer maintenant, ca sera fait au fur et à mesure par la suite.
III.2. Session Exposure
La base de données
Pour se protéger de l'attaque dite "Session Exposure", nous avons
besoin de stocker des données dans une base de données. Nous
allons donc créer les tables nécessaires.
Code SQL pour la création de la table |
CREATE TABLE `sessions` (
`session_id` varchar (32 ) NOT NULL ,
`session_data` text NOT NULL ,
`session_time` datetime NOT NULL ,
`session_browser` varchar (255 ) NOT NULL ,
PRIMARY KEY (`session_id`)
);
|
L'handler de sessions
Il existe deux façon pour stocker les sessions dans une
base de données. Soit vous le faite comme dans mon ancien
article, c'est à dire à la main; solution qui est loin d'être idéal.
Soit vous utilisez l'handler de sessions que nous fournit PHP
qui permet une gestion plus simple des sessions. C'est évidemment
cet dernière option que nous allons utiliser.
L'handler de sessions s'utilise via la fonction
session_set_save_handler.
Cette fonction demande six autres fonctions que l'on est obligé
de créer.
Tout d'abord on va créer l'interface qui contiendra les
fonctions nécessaires à l'handler de sessions. L'interface
ne sert pas à grand chose mais est juste la pour avoir
une hiérarchie des classes propre.
Interface avec les méthodes pour l'handler de session et la vérification des données de Yume/Session/Interface.php |
<?php
interface Yume_Session_Interface
{
public function _open($ save_path , $ name );
public function _close();
public function _read($ id );
public function _write($ id , $ data );
public function _destroy($ id );
public function _gc($ maxLifeTime );
public function checkInfo();
}
?>
|
Ensuite, vient la configuration de l'handler de session. Celle-çi
doit se faire obligatoirement avant le
session_start()
bien entendu.
Configure l'handler de session dans la méthode start() de Yume_Session_Abstract (Yume/Session/Abstract.php) |
if (! session_set_save_handler(
array ($ this , ' open ' ),
array ($ this , ' close ' ),
array ($ this , ' read ' ),
array ($ this , ' write ' ),
array ($ this , ' destroy ' ),
array ($ this , ' gc ' )
))
{
throw new Exception (' Session handler error. ' );
}
|
Une méthode
start() sera utilisée pour initialiser quelques
variables tels que le
maxlifetime utilisé pour le
garbage collector, le temps de début de la session et
pour démarrer la session via la fonction
session_start().
Enfin, il y aura une méthode supplémentaire non obligatoire
qui permet de définir les paramètres pour la ressource de
stockage (par exemple les informations pour la connexion
à la base de données). Ces informations peuvent aussi
être passées en paramètre à la méthode start().
Méthode start() et setRessourceOptions() de Yume_Session_Abstract (Yume/Session/Abstract.php) |
public function start($ params = false )
{
if ($ params )
{
$this ->setRessourceOptions ($ params );
}
if ($ this instanceof Yume_Session_Database)
{
if (empty($this ->_ressourceOptions [ ' hostname ' ] ) | | ! is_string($this ->_ressourceOptions [ ' hostname ' ] ))
{
throw new Exception (' Unknown database host. ' );
}
elseif (empty($this ->_ressourceOptions [ ' dbname ' ] ) | | ! is_string($this ->_ressourceOptions [ ' dbname ' ] ))
{
throw new Exception (' Unknown database name. ' );
}
elseif (empty($this ->_ressourceOptions [ ' username ' ] ) | | ! is_string($this ->_ressourceOptions [ ' username ' ] ))
{
throw new Exception (' Unknown database username. ' );
}
elseif (! is_string($this ->_ressourceOptions [ ' password ' ] ))
{
throw new Exception (' Unknown database password. ' );
}
}
elseif ($ this instanceof Yume_Session_File)
{
if (isset($this ->_ressourceOptions [ ' save_path ' ] ) & & is_string($this ->_ressourceOptions [ ' save_path ' ] ) = = false )
{
throw new Exception (' Invalid save path. ' );
}
else if (empty($this ->_ressourceOptions [ ' save_path ' ] ))
{
$this ->_ressourceOptions [ ' save_path ' ] = session_save_path();
}
}
if (! session_set_save_handler(
array ($ this , ' open ' ),
array ($ this , ' close ' ),
array ($ this , ' read ' ),
array ($ this , ' write ' ),
array ($ this , ' destroy ' ),
array ($ this , ' gc ' )
))
{
throw new Exception (' Session handler error. ' );
}
$this ->_maxLifeTime = ini_get(' session.gc_maxlifetime ' );
$this ->_time = date(' Y-m-d H-i-s ' , time());
session_start();
return $ this ;
}
public function setRessourceOptions(array $ options )
{
foreach ($ options as $ option = > $ value )
{
$this ->_ressourceOptions [ $ option ] = $ value ;
}
return $ this ;
}
|
Voici les différentes méthodes à creer :
open
open($save_path, $name)
Cette méthode à pour but d'initialiser les ressources que
vous allez utiliser pour stocker les sessions. Cette méthode
prend deux arguments qui sont le chemin vers le répertoire
où stocker les sessions et le nom des sessions.
Dans notre cas ni l'un, ni l'autre n'a d'utilité, ni la méthodes
elle-même car la connexion à la base de donnée est effectuée
ailleurs dans la classe.
Méthode open() de Yume_Session_Database (Yume/Session/Database.php) |
public function _open($ save_path , $ name )
{
if ($this ->_ressource = = null )
{
$this ->_ressource = new PDO
(
' mysql:host= ' . $this ->_ressourceOptions [ ' hostname ' ] . ' ;dbname= ' . $this ->_ressourceOptions [ ' dbname ' ] ,
$this ->_ressourceOptions [ ' username ' ] ,
$this ->_ressourceOptions [ ' password ' ]
);
$this ->_ressource ->setAttribute (PDO: : ATTR_ERRMODE, PDO: : ERRMODE_EXCEPTION);
}
return true ;
}
|
close
close()
Cette méthode est utilisée pour fermer les ressources. Elle ne
sera pas non plus utilisée dans notre cas.
Méthode close() de Yume_Session_Database (Yume/Session/Database.php) |
public function _close()
{
return true ;
}
|
read
read($id)
Comme son nom l'indique cette méthode sert à lire les données
contenu dans la session. Elle prend comme paramètre
l'identifiant de session.
Cette méthode doit impérativement retourner une chaîne de
caractères, même si celle-ci est vide !
Méthode read() de Yume_Session_Database (Yume/Session/Database.php) |
public function _read($ id )
{
$ sql = $this ->_ressource ->prepare
("
SELECT session_data
FROM sessions
WHERE session_id = : id
" );
$sql ->bindParam (' :id ' , $ id );
$sql ->execute ();
$ data = $sql ->fetch (PDO: : FETCH_ASSOC );
$ data = $ data [ ' session_data ' ] ;
if (empty($ data ))
{
$ data = ' ' ;
}
return $ data ;
}
|
write
write($id, $data)
Cette méthode permet d'écrire des données dans le contenu
d'une session. Elle prend comme paramètre l'identifiant de
sessin et bien entendu les données à écrire.
Méthode write() de Yume_Session_Database (Yume/Session/Database.php) |
public function _write($ id , $ data )
{
$ sql = $this ->_ressource ->prepare
("
UPDATE sessions
SET session_data = : data ,
session_time = : time ;
WHERE session_id = : id
" );
$sql ->bindParam (' :id ' , $ id );
$sql ->bindParam (' :data ' , $ data );
$sql ->bindParam (' :time ' , $this ->_time );
$sql ->execute ();
if ($sql ->rowCount () = = 0 )
{
$ sql = $this ->_ressource ->prepare
("
INSERT INTO sessions ( session_id , session_data , session_time )
VALUES ( : id , : data , : time ) ;
" );
$sql ->bindParam (' :id ' , $ id );
$sql ->bindParam (' :data ' , $ data );
$sql ->bindParam (' :time ' , $this ->_time );
$sql ->execute ();
}
return true ;
}
|
destroy
destroy($id)
Cette méthode supprime la session. Elle prend comme
paramètre l'identifiant de session.
Méthode destroy() de Yume_Session_Database (Yume/Session/Database.php) |
public function _destroy($ id )
{
$ sql = $this ->_ressource ->prepare
("
DELETE FROM sessions
WHERE session_id = : id
" );
$sql ->bindParam (' :id ' , $ id );
$sql ->execute ();
return true ;
}
|
gc
gc($maxLifeTime)
Cette dernière méthode joue le rôle de ramasse miette (ou
plus couramment appelé "garbage collector").
Chaque fois qu'une session est ouverte, la probabilité que
cette méthode est exécutée dépend d'un petit calcul :
session.gc_probability/session.gc_divisor. Un nombre aléatoire
est alors généré et si celui-ci est inférieur au résultat du calcul,
la méthode est exécutée.
Par exemple 1/100 veut dire qu'il y a 1% de chance que le
garbage collector soit exécuté à chaque requête.
La méthode efface alors toutes les sessions qui auraient un
âge supérieur à la variable passée en paramètre.
Méthode gc() de Yume_Session_Database (Yume/Session/Database.php) |
public function _gc($ maxLifeTime )
{
$ sql = $this ->_ressource ->prepare
("
DELETE FROM sessions
WHERE DATE_ADD ( session_time , INTERVAL : maxlifetime SECOND ) < NOW ( )
" );
$sql ->bindParam (' :maxlifetime ' , $this ->_maxLifeTime );
$sql ->execute ();
return true ;
}
|
III.3. Session Fixation
III.4. Session Sniffing
III.5. Session Prediction
III.6. Session Hijacking
III.7. Session Poisoning et Session Injection
IV. Conclusion


Copyright © 2008 Adrien Pellegrini.
Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de
son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 €
de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.