Sécurisation stateless PHP avec jeton (token) - protection CSRF en PHP
Par François Lasselin le jeudi, octobre 1 2009, 11:01 - Sécurité - Lien permanent
Habituellement, les porteurs du contexte d'une application sont les sessions. Dans une architecture stateless, le serveur répond à chaque requête de manière indépendante. L'utilisation des sessions du serveur est donc impossible. Les sessions sont une brique nécessaire à la sécurisation d'une application mais ne sont pas suffisante à la protection contre les attaques CSRF. Les ressources francophones sur le sujet sont peu nombreuses. Voici donc une contribution à un problème encore trop peu connu.
Le Stateless
Dans une architecture stateless, le serveur répond à chaque requête de manière indépendante:
- L'avantage recherché de l'utilisation du stateless c'est la capacité à monter en charge simplement en ajoutant des serveurs ( et un load balancer). En stateless, on n'a pas à mettre en place de système 'complexe' de partage du contexte utilisateur. L'architecture d'un service stateless est donc complétement scalaire.
- L'inconvénient c'est qu'en l'absence d'état partagé entre les serveurs. Il n'y a pas conservation du contexte de la visite par le serveur sous peine d'incohérence (la répartition de charge aiguillant les requêtes successives vers des machines différentes). Pour un site de publication de contenu journalistique dans lequel les utilisateurs se contentent de lire un contenu statique, maintenir un contexte utilisateur n'a pas d'importante mais pour une application web ou un système d'information c'est essentiel.
Les sessions et les CSRF
Habituellement, les porteurs du contexte d'identification sont les sessions. La session est un ensemble d'informations enregistré sur le serveur et associé à un numéro unique qui identifie l'utilisateur grâce à un cookie envoyé au navigateur. Ce dispositif d'authentification a des limites. En effet, le navigateur enregistre les paramètres du cookie et de ce fait, toutes les requêtes réalisées (intentionnelles ou pas) à partir du navigateur vers le domaine concerné contiendront les informations. Ceci fait que le mécanisme peut-être détourné. Par exemple, si l'utilisateur alors qu'il est authentifié sur l'application visée reçoit un mail (ou visite un site piégé) contenant ce type de balise :
<img src='http://www.domaine.fr/?action=change_mdp&mdp=toto'>La requête vers l'image va s'exécuter avec le contexte de l'utilisateur dans son navigateur et bénéficiant ainsi du cookie permettant d'authentifier la requête comme étant recevable. C'est ce qu'on appel une vulnérabilité au Cross-site request forgery (CSRF).
Par ailleur, dans un environnement Stateless, on ne dispose même pas des sessions. Ce qui pose un problème de sécurité complexe: Comment sécuriser l'application ?
Sécurisation en stateless
Le principe stateless oblige à faire porter l'information par le client(navigateur) tout en signant les informations pour vérifier leur authenticité. C'est un principe de base, on ne peut pas faire confiance au information envoyé par le client.On pourrait aussi stocker la session en base de données mais les performances s'en trouverait dégradé.
On pourrait utiliser Memcached pour stocker les sessions, mais on souhaite conserver une application scalaire strictement.
La sécurisation va donc se baser sur 3 principes :
- Comparaison : vérifier que le token reçu à une requête est bien celui qui a été envoyé par le serveur le coup d'avant
- le token est imprédictible: le client (surtout l'attaquent) ne doit pas pouvoir générer lui même un token. Pour cela on utilise une clef secrete côté serveur pour encrypter le jeton. On parle généralement de grain de sel.
- limiter dans le temps. Le jeton a une durée de vie limité dans le temps par le serveur (la durée de vie du cookie n'est pas une information signée, donc pas fiable)
La vérification de l'url permet également de s'assurer du cheminement cohérent de l'utilisateur.
Dans le cadre de la réalisation d'une application web stateless à forte charge voici la trame de script mettant en œuvre un jeton de session côté navigateur. N'allez pas penser que ce script prêtant être la réponse absolu à l'insécurité. La sécurité absolu n'existe pas (comme pour les vélos, l'antivol absolu n'existe pas). Par ailleurs, ce script est incomplet comme indiqué par les commentaires inlines. Notamment, le contrôle des contraintes d'url (vérifié le cheminement logique) n'est pas implémenté. De plus, le formulaire gagnerait à être signé avec un deuxième jeton. Toutefois, je n'ai pas trouvé une manière industrielle de le faire.
// le user_id devrait être issus du login
$user_id=42;
// la clef secrete (grain de sable) et la durée de validité devraient être stockés
// dans un fichier de configuration
$secret_key= "chaine aleatoire";
$validity_time=600;
// le jeton est composé de la clef secrète, de l'url du service et du user-agent
$token_clair=$secret_key."https://".$_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"]
.$_SERVER['HTTP_USER_AGENT'];
// les informations (ici: id de l'utilisateur et la date de création du jeton)
// vont être transmis en clair dans un cookie et ajouté au jeton pour être signé.
// On pourra ainsi s'assurer de leur authenticité.
$informations=time()."-".$user_id;
// On encode le jeton
$token = hash('sha256', $token_clair.$informations);
// On poste les cookies
setcookie("session_token", $token, time()+$validity_time);
setcookie("session_informations", $informations, time()+$validity_time);
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title></title>
</head>
<body>
<form method="POST" action="something.php">
<input type="text" name="message"><br />
<input type="submit">
</form>
</body>
</html>
le formulaire de la page est posté vers :
// bis repetita, à mettre dans un fichier de configuration
$validity_time=600;
$secret_key= "chaine aleatoire";
// ici, on doit renouveller le cookie en générant un jeton actualité (notament l'heure et url)
// on calcul le token prédictible, sauf la clef secrete connue du serveur uniquement
$token_clair= $secret_key.$_SERVER['HTTP_REFERER'].$_SERVER['HTTP_USER_AGENT'];
//on recrypte avec les informations transmises dans le cookie en clair
$token = hash('sha256', $token_clair.$_COOKIE["session_informations"]);
//On compare le hash calculé avec le hash passé en cookie
if(strcmp($_COOKIE["session_token"], $token)==0)
{
// S'ils sont identiques on peut récupérer les informations
echo "signature ok<br>\n";
list($date, $user) = split('[-]', $_COOKIE["session_informations"]);
// On vérifie que la session n'est pas expirée
if($date+ $validity_time>time() AND $date <time())
{
// On peut aussi vérifier que l'url en referer est cohérente avec l'action entreprise
// Par exemple que l'action suppression a bien été précédé de l'action de confirmation
echo "session en cour de validité<br>";
echo "user id:".$user."<br>\n";
}
else
{
echo "wrong timing<br>";
exit;
}
}
else
{
echo "token check failed<br>";
exit;
}
?>
On peut aller plus loin avec d'autres protections :
- Signer le formulaire : Il est beaucoup plus efficace de signer le formulaire. Ainsi, au lieu de transmettre le jeton par un cookie. On transmet le jeton dans le corps du formulaire. De cette manière, cette données n'est pas dans le contexte du navigateur et ne peut pas être exploité par une autre page. Toutefois, cette méthode nécessite l'intervention au coup par coup d'un développeur et pour chaque formulaire.
- Pour chaque session utilisateur, générer les noms de pages aléatoirement. Ainsi, l'attaque csrf est bloquée par l'ignorance de l'url a appeler. Ce dispositif nécessite la mise en place d'un système d'encodage/décodage des urls et surtout d'un taggage spécifique de la vue pour réaliser les appels d'encodage d'url.
- Ré-authentification : Avant toute opération sensible, on ré-authentifie l'utilisateur. Ce qu'on voit fréquemment sur les site bancaire (par exemple : avant de confirmer un virement)
- Casser les frames. Typiquement avec le code suivant:
if (top != self) {
top.location.href = 'http://www.domainedusite.fr/';
} - utiliser un CAPTCHA (cryptogramme visuel) avant une opération sensible.
Industrialisation & Framework
L'intérêt d'une telle solution est sa capacité à être industrialisée de manière transparente. Ainsi, il est intéressant d'exploiter le design pattern MVC 2 pour mutualiser le renouvellement du jeton et réaliser les tests de validité du jeton de proche en proche. Par ailleurs, cela permet d'éviter de solliciter le développeur pour gérer un problème transversale supplémentaire.Les frameworks peuvent offrir des réponses aux attaquent CSRF. Symfony à travers son plug-in sfCSRFPlugin et son scafolding ajoute automatiquement des jetons dans tout les formulaires. C'est donc complétement transparent pour le développeur (bien que Symfony ne soit pas forcément le meilleur choix pour une application stateless à forte charge). Malheureusement, on ne trouve pas de dispositif similaire dans le Framework Zend.
La discussion continue ailleurs
URL de rétrolien : http://blog.nalis.fr/index.php?trackback/55
Commentaires
Merci pour ce tuto et ce code.
Bonjour,
Merci pour ce tuto j'ai juste une question :
Lors de la validation du formulaire il me dit que la session est valide ok mais après lors d'un rafraichissement de page celui ci me dit "token check failed"
Je n'arrive pas à reproduire ton problème (j'ai testé en http, pas en https).
Est-ce que tu peux réessayer (et en relançant le navigateur) ?
Hum oui j'avais laisser le https merci ;)
En enregistrant la validité dans le cookie, elle ne sert à rien. La date d'expiration du jeton dois être enregistrée côté client sinon, autant ne pas en mettre. Les dates d'expiration des cookies, y a rien de plus facile à modifier.
Ca semble être une très bonne alternatives aux sessions PHP pour les systèmes scallables. J'aurais 2 questions :
- comment gérer les navigateurs qui n'acceptent pas les cookies (les sessions PHP mettent l'id de la session dans l'url) ?
- est-ce que l'encodage "hash" prend au final autant de ressource que si on passait pas du SSL ?
Merci ;)