Sécuriser les cookies de son application web

La plupart des sites utilisent des cookies. Que ce soit pour du tracking ou de la persistance de données, il faut bien avouer que les bonnes pratiques de sécurité ne s'appliquent pas souvent. Pourtant il ne s'agit souvent que d'une option à ajouter pour garantir la sécurité.

Créer un cookie en php est simple, la fonction est setcookie().

L'option http_only

Par défaut un cookie est lisible en javascript. Ce simple code le montre.

<?php
setcookie('user_id', 10, 0, '/', 'orion.dev');
?>
 
Hello!
 
<script>
    alert(document.cookie);
</script>
 

Une fois la page chargée, une alerte affichera le contenu du cookie. Si on peut lire le cookie en javascript, c'est facile de faire un petit bout de code qui va envoyer ce contenu sur une autre URL en ajax, il ne reste plus qu'a injecter ce code sur un site et nous voici avec une attaque XSS basique mais fonctionnelle.

Si l'on peut lire le cookie en javascript, on peut aussi l'écrire.

<?php
setcookie('user_id', 10, 0, '/', 'orion.dev');
 
var_dump($_COOKIE);
?>
 
Hello!
 
<script>
    document.cookie = "username=ulrich; expires=Thu, 17 Dec 2020 12:00:00 UTC; path=/";
    alert(document.cookie);
</script>

Une fois la page chargée, l'alert affichera bien user_id et username. Il suffit de rafraichir la page dans le navigateur pour que le cookie altéré soit envoyé au serveur et l'on retrouvera username dans $_COOKIE. Heureusement les navigateurs modernes ne permettent pas (ou plus) de surcharger, en javascript,  une valeur du cookie qui a été envoyée par le serveur web. Neanmoins, le fait de pouvoir lire le cookie en javascript reste un problème.

La dernière option de la commande setcookie() est http_only et vaut false par défaut. En la passant à true, le cookie envoyé par le serveur web ne sera plus disponible en javascript. Cela n’empêchera pas de créer des cookies en javascript et ils seront disponible coté serveur, mais le vol d'information (comme l'identifiant de session) ne sera plus disponible.

<?php
setcookie('user_id', 10, 0, '/', 'orion.dev', false, true);
 
var_dump($_COOKIE);
?>
 
Hello!
 
<script>
    document.cookie = "username=ulrich; expires=Thu, 17 Dec 2020 12:00:00 UTC; path=/";
    alert(document.cookie);
</script>

Dans Symfony, on retrouve cette option dans la configuration des sessions dans la rubrique framework du fichier config.yml. Cette option est a true par défaut dans les version 3.x et 4.x mais je ne suis pas sûr que c'était le cas pour toutes les versions de symfony 2.

framework:
    session:
        cookie_httponly: true

Cette option peut également être définis dans le fichier de configuration globale php.ini .

L'option secure

Si votre site est en HTTPS et ne propose plus d'HTTP, cette option est pour vous. Elle garantie que le cookie ne sera transmis que si votre connexion est sécurisé. Hum, enfin ..... Cette règle ne s'applique que dans le sens navigateur -> serveur web et non l'inverse. C'est au développeur de vérifier qu'il est bien sur une connexion sécurisé avant d'envoyer un cookie dans le navigateur.

Mais du coup à quoi ça sert? Cela permet d'éviter le vol de cookie via une attaque Man In The Middle, puisque le cookie sera protégé pendant le transfert. C'est très important si votre application permet aux utilisateurs de rester connectés, ce fonctionnement passe par un cookie et dès la première requête au serveur, le navigateur envoie ce cookie.

Il s'agit de la 6ème option de la fonction setcookie() qui par défaut est à false.

setcookie('user_id', 10, 0, '/', 'orion.dev', true, true);

Dans Symfony, on retrouve cette option au même endroit que http_only mais elle est désactivée par défaut sur toutes les versions du framework.

framework:
    session:
        cookie_httponly: true
        cookie_secure: true

Il faudra également être explicite dans la configuration du RememberMe si vous l'utilisez.

security:
    firewalls:
        main:
            remember_me:
                secure: true

Les préfixes

Ils sont assez mal connus, mais supportés par tous les navigateurs sauf ceux de Microsoft. Avec les préfixes, il est possible de forcer le navigateur à ne pas accepter un cookie s'il est mal configuré. Il existe deux préfixes "__Secure-" et  "__Host-".
__Secure- oblige le développeur à ajouter le flag secure à son cookie, sinon il sera ignoré par le navigateur.

setcookie('__Secure-user_id', 10, 0, '/', 'orion.dev', true);

__Host- est plus restrictif, le cookie doit toujours avoir le flag secure, mais également le path à la racine et sans domaine.

setcookie('__Host-user_id', 10, 0, '/', '', true);

Vous ne voyez pas à quoi peuvent servir ces préfixes? Envisagez les comme une sécurité en plus qui vous permettra d'être alerté (un bug peut être une alerte!) si vous vous trompez dans la configuration de vos cookies.

Avec Symfony il faut changer le nom de la session dans le fichier config.y(a)ml pour ajouter le préfixe.

framework:
    session:
        cookie_httponly: true
        cookie_secure: true
        name: __Secure-sessid
 

Si vous utilisez la fonctionnalité RememberMe vous pouvez faire de même dans le fichier security.y(a)ml.

security:
    firewalls:
        main:
            remember_me:
                name: __Secure-REMEMBERME
                secure: true
 

L'option SameSite

Cette option est également très intéressante, elle permet de limiter (pas empêcher) les attaques CSRF car elle indique aux navigateurs de ne pas envoyer le cookie si la requête source n'a pas été faite depuis le site. Pour faire simple, si je mets un lien Github ici et que vous cliquez dessus, le cookie qui vous permet d'arriver authentifié sur github ne sera pas envoyé. Vous vous dites que ce n'est pas très "user friendly", mais si je remplace Github par votre banque, vous en pensez quoi?
Il existe 2 options: lax et strict. Comme son nom l'indique "strict" ne laisse rien passé. Par contre "lax" laissera passé le cookie si la requête HTTP est de type GET.
Coté navigateurs, ça se présente bien, seul Safari est en retard puisque le support est prévu pour la prochaine version (desktop et mobile), par contre Opéra mini n'a pour le moment rien annoncé. Coté PHP, c'est un peu plus compliqué, car le support de SameSite n'arrivera qu'en PHP 7.3

En attendant de migrer sur cette nouvelle version, pour profiter du support de SameSite, il faut donc envoyer le cookie via les headers http.


<?php 
header('Set-Cookie: __Secure-REMEMBERME=5sdf4d65f4; Path=/; Secure; Samesite=lax');

Coté Symfony, le support de SameSite pour le cookie RememberMe sera dispo dans la version 4.2.

security:
    firewalls:
        main:
            remember_me:
                name: __SECURE-REMEMBERME
                secure: true
                samesite: lax
 


Pour facilement contrôler la sécurité de vos cookies et de votre config de façon plus global, je vous conseilles l'observatoire de Mozilla.

Ajouter un commentaire