IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

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

warning - PHP 5

- Extension PHP Data Objects - PDO

- Extension Chiffrement mcrypt

- Quelques notions de programmation orienté objet en PHP.

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; ...).

Serveur mutualisé et son unique dossier /tmp

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.

Solution concernant la faille dite session exposure

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".

Session Fixation - page1.php
Session Fixation - page2.php


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 3 :
Sans aucun doute, la meilleure des solutions est d'utiliser la fonction session_regenerate_id().

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.

Diagramme des classes

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)
	{
		// Récupère les paramètre et les traîte
		if ($params)
		{
			$this->setRessourceOptions($params);
		}
		
		// Vérifie les paramètre
		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();
				}
		}
		
		// Configure l'handler de session
		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.');
		}
		
		// Récupère le maxlifetime
		$this->_maxLifeTime = ini_get('session.gc_maxlifetime');
		
		// Enregistre l'heure de début de la session
		$this->_time = date('Y-m-d H-i-s', time());
		
		// Démarre la session
		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)
{
	// Récupère les données
	$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)
{
	// Met à jour la base de données
	$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();
	
	// Ou alors insère les données
	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




Valid XHTML 1.1!Valid CSS!

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.