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.

<?php
  // 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 :
<?php

// 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.