Protection contre les attaques CSRF

Mise en place d'un système de défense contre Cross-site request forgery

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

CSRF est souvent appelée "l'attaque sur l'inconscient" car la victime, n'effectuant aucune opération dangereuse, ne se rend compte de rien. Cet article montrera quel moyen de protection on utilisera dans notre exemple d'application Spring.

Se protéger contre CSRF sous Spring

Une autre faille de sécurité potentielle concerne CSRF. L'attaque est expliquée plus en détails dans l'article de mon blog consacré aux façons de protéger une application web contre les attaques CSRF.

L'idée est de créer un outil qui va construire un token unique par utilisateur dans toutes les actions qu'il peut effectuer (modification du compte, emprunt d'un livre...). Le token unique sera basé sur 4 éléments : 2 chaînes de caractères qui serviront à "hasher" le token, 1 intention (action que l'utilisateur veut effectuer) et 1 sel (unique par utilisateur, servira à encoder les 3 éléments précédents). Regardons ensemble dans le code :

    <beans:bean id="CSRFProtector" name="CSRFProtector" class="library.security.CSRFProtector">
        <beans:property name="expirationTime"><beans:value type="long">1800000</beans:value></beans:property>
        <beans:property name="hashStart" value="9F.fdsoN33!!!" />
        <eans:property name="hashEnd" value="Nofds$POE*dso.e" />
        <beans:property name="passwordEncoder" ref="passwordEncoder" />
    </beans:bean>

Et la classe Java :

afficher le code

Comment ça fonctionne dans une implémentation ? Pour le constater, on regardera le formulaire d'enregistrement :

@Controller
public class SubscriberController extends FrontendController {
    public String register(@ModelAttribute("subscriber") Subscriber subscriber, Model layout, RedirectAttributes redAtt, HttpServletRequest request) {
        // only code using CSRFProtector's instance is displayed
        try {
            csrfProtector.setIntention("register");
            subscriber.setToken(csrfProtector.constructToken(request.getSession()));
            subscriber.setAction(csrfProtector.getIntention());
            logger.info("Generated token " + subscriber.getToken());
        } catch(Exception e) {
            logger.error("An exception occured on creating CSRF token", e);
        }
    }
    // ...
}

On voit que la sécurisation est banale. Il s'agit juste d'un simple jeu des setters et des getters. La validation est beaucoup plus intéressante. Pour l'effectuer on crée une annotation pouvant être définie au niveau de la classe (@CSRFConstraint). Elle utilise le CSRFConstraintValidator pour valider le token transmis dans la requête avec celui qui est correct. Cette classe de validation implémente l'interface ConstraintValidator. On y injecte deux classes : HttpServletRequest et CSRFProtector. Cette première servira à récupérer la session de l'utilisateur. L'autre s'occupera de générer le token correct. Voici ses éléments regroupés :

afficher le code

Une autre solution, consistant à implémenter l'interface org.springframework.web.servlet.support.RequestDataValueProcessor, sera abordée dans une version améliorée du système.

Bartosz KONIECZNY Sécurité

Une question ? Une remarque ?

*

*

Un conseil Symfony2

Un problème avec la définition des valeurs par défaut pour input type checkbox ?

Si vous rencontrez un problème avec la définition des valeurs par défaut pour un champ du type checkbox sous Symfony2, assurez-vous de la conformité des types de ces valeurs. Par exemple, le code suivant ne va pas fonctionner (le checkboxes ne seront pas sélectionnés pour les valeurs indiquées) :

  private $gifts = array(1 => 'apple', 2 => 'orange');
  public function setPreferedGifts($value = array())
  {
    $vals = array();
    foreach($value as $v => $val)
    {
      $vals[] = $val;
    }
    $this->preferedGifts = $vals;
  }
Par contre, le code suivant fonctionnera correctement (les checkboxes seront sélectionnés pour des valeurs passées dans la boucle foreach) :
  private $gifts = array(1 => 'apple', 2 => 'orange');
  public function setPreferedGifts($value = array())
  {
    $vals = array();
    foreach($value as $v => $val)
    {
      $vals[] = (int)$val;
    }
    $this->preferedGifts = $vals;
  }
Pour résumer, si le tableau avec les choix ($gifts dans l'exemple) contient les clés qui sont des integers, les valeurs par défaut doivent aussi être des integers.