une classe pour sécuriser les sessions

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

05 août 2006, 18:03

J'ai retrouvé ça dans mes cartons, je pense que ça peut éventuellement être utile. En gros, il s'agit d'un singleton PHP5 englobant la superglobale $_SESSION, avec en plus des actions permettant d'empêcher au maximum le "session fixation" à chaque page :
- session_regenerate_id().
- stockage et vérification de l'ip du visiteur en variable de session.
- stockage et vérification d'un hash composé de l'identifiant du navigateur, de l'adresse ip, d'un sel aléatoire, et de l'identifiant de session.

Je n'ai pas repris le code, et il y a des défauts qui sautent aux yeux (par exemple que la vérification de l'ip est incluse dans la vérification du hash, et donc inutile).

C'est livré comme toujours "as-is", et je vous documente ça là vite fait (*) :langue:
Instanciation
Session implémente le Singleton. On n'appelle donc pas son constructeur, mais on rappelle l'instance statique :
$session = Session::Instance();
Utilisation
Les variables de session sont les variables membres de la classe. Evitez d'utiliser les variables de session "_random" et "_instance" je crois que ça provoquerait une erreur.
// enregistrer une variable de session
$session->maVariable = "ma valeur";

// vérifier qu'une variable existe
if (isset($session->maVariable)) {
  echo "OK, elle est en session";
}

// détruire une variable
unset($session->maVariable);
Méthodes

static Session Instance()
Rappelle l'instance unique de l'objet Session (singleton).

array all()
Renvoie la liste des noms de variables définies en session.

void unsetAll()
Détruit toutes les variables de session.

Exemple d'utilisation, le compteur
$session = Session::Instance();

if (!isset($session->counter)) {
    $session->counter = 0;
}
$session->counter += 1;

echo "COUNTER={$session->counter}";
<?php

class Session
{

    private static $_instance;
    private $_random;

    private function __construct()
    {
        $this->_random = new Random();
        if (headers_sent($filename,$lineno)) {
            throw new Exception('Headers already sent by '.$filename.' (line '.$lineno.') : cannot start session');
        }
        session_start();
        $this->__unset('sec_corrupted');
        $this->checkIp();
        $this->checkHash();
        session_regenerate_id();
        $this->setIp();
        $this->setHash();
    }

    public static function Instance()
    {
        if (!self::$_instance) {
            self::$_instance =& new self();
        }
        return self::$_instance;
    }

    private function __get($name)
    {
        return $_SESSION[$name];
    }

    private function __set($name, $value)
    {
        return $_SESSION[$name] = $value;
    }
    
    private function __unset($name)
    {
        unset($_SESSION[$name]);
        session_unregister($name);
    }

    private function __isset($name)
    {
        return isset($_SESSION[$name]);
    }
    
    public function all()
    {
        return array_keys($_SESSION);
    }

    private function checkIp()
    {
        $ip = $_SERVER['REMOTE_ADDR'];
        if ($this->__isset('sec_ip')) {
            if ($this->__get('sec_ip') != $ip) {
                // corrupted !
                $this->unsetAll();
                $this->__set('sec_corrupted', 'ip');
            }
        }
    }
    
    private function setIp()
    {
        $ip = $_SERVER['REMOTE_ADDR'];
        $this->__set('sec_ip', $ip);
    }

    private function generateSalt()
    {
        if (!$this->__isset('sec_salt')) {
            $this->__set('sec_salt', $this->_random->generateAlnum(8));
        }
    }

    private function getHash()
    {
        $this->generateSalt();
        $hash = md5(
            $this->__get('sec_salt') . ':' .
            $_SERVER['HTTP_USER_AGENT'] . ':' . 
            $_SERVER['REMOTE_ADDR'] . ':' . 
            session_id());
        return $hash;
    }

    private function checkHash()
    {
        $hash = $this->getHash();
        if ($this->__isset('sec_hash')) {
            if ($this->__get('sec_hash') != $hash) {
                // corrupted !
                $this->unsetAll();
                $this->__set('sec_corrupted', 'hash');
            }
        }
    }

    private function setHash()
    {
        $hash = $this->getHash();
        $this->__set('sec_hash', $hash);
    }

    public function unsetAll()
    {
        $vars = $this->all();
        foreach ($vars as $var) {
            $this->__unset($var);
        }
    }

}


?>
Cette classe utilise la class "Random" ci-dessous :
<?php

class Random
{

    public function __construct()
    {
        $this->seed(microtime(true));
    }

    public function seed($seed)
    {
        mt_srand($seed);
    }

    public function get($min = null, $max = null)
    {
        if ($max === null && $min !== null) {
            return false;
        }
        if ($min === null) {
            return mt_rand();
        }
        else {
            return mt_rand($min, $max);
        }
    }

    public function generateAlpha($len, $case_sensitive = false)
    {
        $chars = 'abcdefghijklmnopqrstuvwxyz';
        if (!$case_sensitive) {
            $chars .= strtoupper($chars);
        }
        return $this->generateString($chars);
    }

    public function generateAlnum($len, $case_sensitive = false)
    {
        $chars = 'abcdefghijklmnopqrstuvwxyz';
        if (!$case_sensitive) {
            $chars .= strtoupper($chars);
        }
        $head = $this->generateString($chars, 1);
        $chars .= '0123456789';
        $tail = $this->generateString($chars, $len-1);
        return $head . $tail;
    }

    public function generateString($chars, $len)
    {
        $string = '';
        $nb_chars = strlen($chars);
        for ($i=0; $i<$len; ++$i) {
            $string .= $chars{$this->get(0, $nb_chars)};
        }
        return $string;
    }

}

?>
(*) C'est du vieux code, je l'ai relu et retesté il est OK, mais - honte sur moi - n'est pas commenté ni documenté. L'objet c'est bien on comprend vite de quoi on parle, mais si on me le demande j'ajouterai des commentaires.
Modifié en dernier par naholyr le 05 août 2006, 20:02, modifié 1 fois.

Mammouth du PHP | 19672 Messages

05 août 2006, 19:39

Très instructif :) Et effectivement, quelques commentaires seraient sûrement tout à fait bienvenus ;)
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

05 août 2006, 20:02

Ah oui tiens ça je n'en ai pas parlé, en attendant les commentaires.

Les variables de session suivantes sont "réservées" (quoiqu'aucune vérification ne soit faite - c'est d'ailleurs une erreur - mais les écraser invaliderait automatiquement la session) :
sec_ip est l'adresse IP du visiteur. Valeur fixe (le temps de la session).
sec_salt est un sel aléatoirement généré à la création de la session. Valeur fixe (le temps de la session).
sec_hash est le "hash" du visiteur (dépend de sec_salt, sec_ip, le user-agent, et l'id de la session, et change donc à chaque page).
sec_corrupted n'est défini que s'il y a eu corruption de la session. S'il est défini, alors la session est vidée de toute variable, et sec_corrupted vaut "ip" ou "hash" selon lequel des deux tests a échoué.