Cross-site request forgery, l'attaque sur l'inconscient

mieux protéger une application internet

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com
Dans un langage simplifié, CSRF (Cross-site request forgery) signifie exécution inconsciente d'une requête. Il peut s'agir par exemple de la suppression d'un commentaire du blog après l'ouverture d'un mail ou d'un site suspect.

Dans le passé c'était un vrai problème. Plusieurs sites majeurs avaient du mal à identifier cette vulnérabilité. En 2008 YouTube.com, The New York Times et même la banque ING y étaient sensibles. Même Gmail dans ses premières versions (en 2007) n'a pas été épargné.

L'exemple d'attaque via e-mail
L'attaque peu standard qui se base sur le message e-mail. ATT signifie l'attaquant et VIC la victime.

L'attaque se base sur l'envoi du mail avec une petite image (1px sur 1px) qui a comme source l'adresse qui supprime un élément du site.com .


<img src="http://site.com/delete_item/id/30" width="1" height="1" border="0" />


La victime n'a même pas besoin de cliquer. Il suffit qu'elle accepte de télécharger le fichier dans son client mail. Cela signifiera automatiquement l'exécution de l'action "delete_item/id/30". Si la victime a laissé sa session ouverte sur site.com, le téléchargement déclenchera également la suppression de l'élément 30.

L'exemple d'attaque via site Internet
Une attaque via message e-mail peut s'avérer un peu compliqué si l'on ne dispose pas d'adresses e-mails des utilisateurs du site. Un moyen plus simple est une page HTML avec exactement le même code. On illustrera ce cas avec des simples pages PHP et une petite table MySQL.

D'abord on crée une table "test" avec id et title comme colonnes. On y insère 4 lignes dont les identifiants sont 1, 2, 3 et 4. Côté serveur on aura 2 fichiers. Le premier va initier la session.

login.php

session_start();
$_SESSION['id'] = 20;

print_r($_SESSION);


Le deuxième fichier va se charger de supprimer l'élément passé dans le paramètre de l'URL.
delete.php

session_start();
mysql_connect("host", "user", "password");

$query = "USE tester";
mysql_query($query);
print_r($_SESSION);
if((int)$_SESSION['id'] > 0) {
echo 'Delete'.$_GET['id'];
$sql = "DELETE FROM test WHERE id = ".(int)$_GET['id']."";
mysql_query($sql);
}


La page de l'attaquant sera aussi simple :
danger.html

<img src="http://attack_page/csrf/delete.php?id=3" />
This is a funny image


Dans le test on procédera de la manière suivante. D'abord on ouvre le 1er fichier (login.php) et on initialise la session. Ensuite on appelle le 2e fichier (delete.php) avec l'url http://my_page/csrf/delete.php?id=1 . Le premier élément a été correctement supprimé.

A la fin on accède à la page danger.html. En vérifiant ensuite quelles lignes sont dans la base, on s'aperçoit que seulement les éléments 2 et 4 y sont stockés. L'élément 3 a été supprimé par une petite image du document danger.html.

Maintenant on procède à la 2e partie du test. On modifie danger.html en :

<img src="http://attack_page/csrf/delete.php?id=2" />
This is a funny image


On l'appelle maintenant à partir d'un autre navigateur. Ensuite on se connecte à notre serveur MySQL pour vérifier le nombre d'éléments. Il y en a toujours 2 car la session n'a pas été initiée pour ce navigateur, donc la suppression n'a pas pu être exécutée.

Comment se protéger ?
Pour protéger l'application web contre l'attaque CSRF, on devra utiliser un "ticket system" qui distribue pour chaque action un ticket unique. Ce billet, comparé à celui stocké dans la session, autorise la personne à exécuter ou pas une action donnée.

Dans un deuxième temps il faudra définir la politique d'exécution des actions. Il s'agit de de préciser quelles opérations sont permises pour un type d'utilisateur donné.

Un troisième moyen est la vérification si l'en-tête Referrer provient de l'adresse de notre site. Cependant, cela ne peut pas être le seul moyen de protection. Cette vérification devrait être juste un niveau supplémentaire car il est possible de falsifier le header Referrer.

L'article écrit en rythme de:
Kénédy - Reviens-moi
Bartosz KONIECZNY 01-05-2011 07:11 sécurité des applications web
Moi

Développeur d'applications Internet et journaliste passionné par l'adjectif français. Un aigle polonais orienté vers la progression, volant très haut et écoutant du zouk après les matches du foot français.

Vous appréciez mon travail ?

Pour contribuer au développement de ce site, ou pour remercier pour des articles rédigés, vous pouvez faire un don.

Un conseil Symfony2

Un problème avec la définition des valeurs par défaut pour input type checkbox ?

Si vous rencontrez un problème avec la définition des valeurs par défaut pour un champ du type checkbox sous Symfony2, assurez-vous de la conformité des types de ces valeurs. Par exemple, le code suivant ne va pas fonctionner (le checkboxes ne seront pas sélectionnés pour les valeurs indiquées) :

  private $gifts = array(1 => 'apple', 2 => 'orange');
  public function setPreferedGifts($value = array())
  {
    $vals = array();
    foreach($value as $v => $val)
    {
      $vals[] = $val;
    }
    $this->preferedGifts = $vals;
  }
Par contre, le code suivant fonctionnera correctement (les checkboxes seront sélectionnés pour des valeurs passées dans la boucle foreach) :
  private $gifts = array(1 => 'apple', 2 => 'orange');
  public function setPreferedGifts($value = array())
  {
    $vals = array();
    foreach($value as $v => $val)
    {
      $vals[] = (int)$val;
    }
    $this->preferedGifts = $vals;
  }
Pour résumer, si le tableau avec les choix ($gifts dans l'exemple) contient les clés qui sont des integers, les valeurs par défaut doivent aussi être des integers.