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

Problème avec les valeurs par défaut pour un champ type checkbox ?

La gestion des formulaires sous Symfony2 correspond parfaitement au slogan de jQuery "write less, do more". En effet, il suffit de déterminer les validateurs une fois et après seulement les adapter en fonction du groupe du formulaire. Egalement la définition des champs est très intuitive. La récupération des données saisies, en cas d'une erreur de validation, est aussi automatique. Cependant, il se peut que vous renctonreriez un problème avec les champs du type checkbox. Supposons, qu'on veut construire un checkbox qui prendra pour valeur des chiffres (integers) correspondant aux identifiants aux apparements recherchés (1 pièce, 2 pièces, 3 pièces etc.) :

protected static $types = array(1 => '1 room', 2 => '2 rooms', 3 => '3 rooms');
Après une validation incorrecte, on retourne sur la page avec le formulaire pré-rempli. Pour être sûr que la liste des checkboxes va avoir les champs pré-cochés (par exemple 1 room et 3 rooms), il faut s'assurer que les types des valeurs sauvegardées sont égales à celles de la variable statique $types. Souvent il faut faire une boucle pour régler un éventuel problème avec les valeurs pré-cochés dans multiple checkboxes sous Symfony2 :
    foreach($values as $v => $val)
    {
      $vals[] = (int)$val;
    }