Se prémunir des failles CSRF en PHP

Quand on parle sécurité pour un site web, la première chose qui nous vient à l’esprit c’est l’injection sql, ou encore le vol de sessions qui sont les grand classique du hack.
Cependant une autre faille moins connue mais tout aussi dangereuse vous guète : La faille Cross-Site Request Forgeries (CSRF).

Le principe

Il est très simple , on utilise l’utilisateur pour exécuter un fichier auquel on à normalement pas accès. Voici un exemple très simple :
Admettons que sur votre site , dans votre admin vous ayez une page du type supprimer_utilisateur.php?idUser=xxx . Vous vous pensez à l’abri car il faut être admin pour accéder à cette page et c’est là toute la subtilité de la faille.
Un de vos membres pas très sympathique poste un message sur votre forum, ce message contient un lien vers une image ou toute chose capable d’exécuter du code. Vous allez donc naïvement consulter le lien de votre membre et sans vous en rendre compte vous aurez exécuté votre page de suppression avec les paramètres voulu par le hacker.
C’est ce qui rend cette faille très dangereuse, c’est totalement invisible pour la personne piégée !

Comment limiter les risques ?

  • La première chose est de limiter les paramètre en GET et leur préférer le POST plus discret (mais tout aussi modifiable).
  • Demander des confirmations à l’utilisateur sur les actions critiques. Avant de lancer une suppression demander à l’utilisateur si il est réellement certains , voir même lui demander une passphrase secrète.
  • Utiliser des tokens pour toutes les requêtes.

Ce qui ne marche pas : Ce baser sur le référant contenu dans la requête HTTP. C’est modifiable comme toute la requête.

Les Tokens

Ou jetons en bon français est la solution la plus robuste. Elle consiste à attribuer un jeton à l’utilisateur avant d’effectuer une requête (typiquement en arrivant sur le formulaire) et de vérifier ce jeton au moment d’exécuter l’action. Les jetons étant regénéré constamment et ayant une durée de vie courte il est impossible de le prédire à l’avance et donc d’exécuter des requêtes sur votre site depuis des sites/domaines qui ne sont pas prévus pour.

Une classe PHP à la rescousse :

class Util_Token
{
 /**
  * Type d'erreur retournée à la vérification du token
  * 1 = Token non passé en paramètre
  * 2 = token recu != token généré
  * 3 = token expiré
  * @var int
  */
 static public $error = 0;
 
 /**
  * Génère un token et le stocke en session
  *
  * @param int $ttl Durée de vie du token en minute
  */
 static public function genToken($ttl = 15)
 {
  if(!isset($_SESSION))
   session_start();
   
  $token  = hash('sha1',uniqid(rand(),true));
  $rand   = rand(1,20);
  //Sha1  = 40 caractères => 20 de longeur max
  $token  = substr($token,$rand,20);
  $ttl *=60;
  $_SESSION['csrf_protect']    = array();
  $_SESSION['csrf_protect']['ttl']  = time()+$ttl;
  $_SESSION['csrf_protect']['token']  = $token;
 }
 
 /**
  * Récupère le token
  *
  * @return string
  */
 static public function getToken()
 {
  if(isset($_SESSION['csrf_protect']) && !empty($_SESSION['csrf_protect']))
   return $_SESSION['csrf_protect']['token'];
  else
   throw new Util_ExceptionHandler('No token available');
 }
 
 /**
  * Récupère le timestamp de durée de vie
  *
  * @return int
  */
 static public function getTTL()
 {
  if(isset($_SESSION['csrf_protect']) && !empty($_SESSION['csrf_protect']))
   return $_SESSION['csrf_protect']['ttl'];
  else
   throw new Util_ExceptionHandler('No token available'); 
 }
 
 /**
  * Vérifie la validité du token
  *
  * @return boolean
  */
 static public function checkToken()
 {
  if(!isset($_SESSION))
   throw new Util_ExceptionHandler('Can\'t check token if there is no session available');
   
  if(isset($_REQUEST['csrf_protect']) && !empty($_REQUEST['csrf_protect']))
  {
   if($_REQUEST['csrf_protect'] == $_SESSION['csrf_protect']['token'])
   {
    if($_SESSION['csrf_protect']['ttl']-time()>0)
    {
     return true;
    }
    else
    {
     self::$error = 3;
    }
   }
   else
   {
    self::$error = 2;
   }
  }
  else
  {
   self::$error = 1;
  }
  return false;
 }
 
 /**
  * Retourn le code erreur
  *
  * @return int
  */
 static public function getError()
 {
  return self::$error;
 }
}

Concrètement comment ça marche ?
Tout d’abord il faut avoir une session d’active , puis générer un token :

Util_Token::genToken();

On récupère ainsi une chaine basée sur un hash d’une id unique. Ce token est enregistré en session.
Il vous reste ensuite à le passer en paramètre lors de vos requêtes dans input hidden ou directement dans votre chaine de paramètre dans le cas de requête ajax par exemple.

Puis sur la page exécutant la requête il vous suffit de vérifier le token avant toute action :

if(Util_Token::checkToken())    //Executer du codeelse    // Mauvais token ou token expiré

Notez que dans son état actuelle la classe nécessite que le paramèter du token sois nommé csrf_protect.

Une réflexion au sujet de « Se prémunir des failles CSRF en PHP »

  1. Chris

    j’ suis entrain de mettre à jour les fonctions de mon framework PHP pour un nouveau projet,
    arrivé à la gestion des token, la class contenait peu de commentaires, un peu le fourbi …
    Merci pour ton article, qui m’a aidé à comprendre la problématique et surtout comment y faire face

    Répondre

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *