Les failles de sécurité de base dans le web (2) : le cross-site scripting (XSS)

Publié le 04 avril 2020 par Abouchard

Si ce n’est pas déjà fait, je vous invite à lire le premier article de cette série. J’y explique le contexte dans lequel j’écris ces articles, et je parle des failles par injection SQL.

Le principe du cross-site scripting

Le cross-site scripting (ou XSS pour faire court) est une faille de sécurité qui arrive assez classiquement lorsque des données saisies par un utilisateur peuvent se retrouver telles quelles dans une page web vue par une autre personne. Si la première personne est mal intentionnée, elle peut ainsi exécuter du code sur le navigateur de la seconde ; c’est la porte ouverte à la récupération d’informations personnelles.

L’usage le plus courant est la récupération des cookies du visiteur, ce qui permet ensuite de se connecter au site en se faisant passer pour cette personne. Cela peut être dramatique sur un site bancaire (détournement d’argent) ou administratif (vol d’identité) ; mais une usurpation d’identité sur un réseau social peut être tout aussi gênante.

Prenons l’exemple d’un site proposant à ses visiteurs de laisser des commentaires. Pour cela, un simple formulaire avec un champ texte fait l’affaire. Le texte saisi par un utilisateur est enregistré en base de données, et lorsqu’un autre internaute consulte la page, les commentaires sont lus depuis la base de données pour être ajoutés dans le corps de la page. Jusque-là tout va bien.
Mais si le commentaire contient du code Javascript, et qu’il se retrouve directement dans la page, il sera exécuté par les navigateurs des visiteurs.

La solution

Normalement, si vous avez fait un minimum de développement web, vous devriez vous dire « Bien sûr, mais personne ne fait ça ! ».
On est d’accord. De manière générale, tout ce qui vient de l’extérieur − saisi par un être humain dans un formulaire, ou bien reçu lors d’un appel à un webservice externe − doit être traité de manière particulière.

La fondation OWASP (Open Web Application Security Project) a écrit une “cheatsheet” sur la prévention des failles XSS, qui donne 14 règles à suivre. Certaines sont assez détaillées et correspondent à des cas assez particuliers. Je vous laisse en prendre connaissance.

Mais pour résumer, il y a deux grands principes à suivre :

  • Ne faites jamais confiance à ce qui vient de l’extérieur.
    Partez du principe que ça contiendra forcément des données véreuses un jour ou l’autre (oui, même si ce sont des données qui viennent d’une API open-data proposée par une administration ou une entreprise sérieuse ; elles sont susceptibles d’avoir leurs propres failles de sécurité).
  • Maîtrisez toujours les données que vous écrivez dans vos pages.
    Lorsque vous insérez dans une page une donnée “externe” (provenant d’un formulaire ou d’un webservice), sachez toujours précisément ce que vous ajoutez et où vous l’ajoutez.

Gérer du texte brut

Si vous avez un formulaire proposant un champ dans lequel vos visiteurs sont susceptibles d’écrire du texte simple (sans mise en forme), vous êtes dans une situation assez simple. Vous pouvez ajouter le texte reçu tel quel en base de données. Par contre, lorsque vous allez insérer le texte dans une page web, il suffira d’échapper les caractères spéciaux, pour être certain qu’aucun symbole ne pourra être interprété par le navigateur comme une balise HTML.

Tout dépend de votre plate-forme. En PHP, on utilisera la fonction htmlspecialchars(). En Smarty, le filtre escape. En Python la fonction cgi.escape(). La fonction escape() en NodeJS. La fonction CGI.escapeHTML() en Ruby. Et ainsi de suite…

Un cas particulier concerne le texte qu’il faut introduire en paramètre GET dans des URL. Là aussi, vous ne devez pas laisser n’importe quoi se glisser, et il faut échapper le texte avec des fonctions adaptées, comme urlencode() en PHP ou encodeURIComponent() en Javascript.

Gérer du HTML

Qu’en est-il si vous avez besoin d’insérer du code HTML ? Par exemple, vous avez pu mettre un éditeur WYSIWYG dans votre site. Vous n’avez peut-être limité les possibilités de mise en page de l’éditeur, de manière à ce que les utilisateurs n’aient que du gras et de l’italique à leur disposition. Mais une personne malintentionnée pourrait en profiter pour vous envoyer n’importe quelle chaîne de caractères ; vous n’avez aucune garantie que le code HTML reçu ne contient pas une balise <script>.

La solution ici est de nettoyer les données en entrée, pour ne stocker en base de données que des informations autorisées. Des bibliothèques sont faites pour ça, comme la OWASP Java Html Sanitizer, ou encore HTMLPurifier. Elles vous permettent de choisir les balises HTML autorisées et leurs attributs éventuels.

Gérer du XML ou du JSON

Lorsqu’il s’agit de gérer des données composites spécifiques comme du XML ou du JSON, il ne faut pas non plus se contenter d’enregistrer les données telles quelles. Le minimum est de vérifier qu’elles respectent leurs formats respectifs.

Pour le XML, une validation par DTD, Schema XML ou Relax NG paraît essentielle. Pour le JSON, il existe plusieurs bibliothèques de validation utilisant le format JSON Schema, comme schema.js ou PHP JSON Schema.

Au minimum, vous pouvez désérialiser le flux entrant, vérifier les données attendues, puis resérialiser avant enregistrement.

Un cas particulier

Il y a un cas un peu spécial, qui n’est habituellement pas listé lorsqu’on parle de XSS, mais qui en est assez proche pour en parler ici : l’injection d’instructions pour déclencher l’exécution de code côté serveur.

Pour illustrer ça, je vais prendre l’exemple des premières versions de PHPBB, un célèbre moteur de forums open source.
À l’époque, toutes les interactions utilisateurs sur un forum passaient par un unique fichier index.php. Suivant le type de page demandé, un paramètre GET était rempli avec le nom du fichier PHP à inclure par le fichier index.php. Le problème, c’est que le nom passé en paramètre était directement passé à la fonction include(), sans la moindre vérification.

Ce qu’il faut savoir, c’est que le PHP offre par défaut la possibilité d’ouvrir des URLs distantes de la même manière que des fichiers locaux. C’est très pratique lorsqu’il s’agit de lire un fichier extérieur ou une API avec la fonction file_get_contents() aussi facilement que s’il s’agissait d’un fichier local.
Lorsque cette fonctionnalité est activée, elle marche avec toutes les fonctions de lecture de fichiers, y compris avec include() et require().

Vous voyez venir le truc. Il suffisait pour un hacker de se connecter à un forum, et mettant en paramètre GET une URL complète vers un autre site. Le forum se retrouvait alors à se connecter sur ce site externe, récupérait du code PHP (qui était envoyé par ce serveur tiers), et l’exécutait gaiement. Je ne vous raconte pas le nombre de bases de données qui ont été effacées, le nombre de sites web qui ont été défacés

La solution, là encore, est de ne jamais faire confiance à une donnée qui vient de l’extérieur. Et un paramètre GET, c’est clairement quelque chose qui peut facilement être manipulable.