La protection CSRF de Django et les requêtes Ajax


La protection contre les attaques CSRF est dans le top 10 des erreurs les plus chiantes en Django, main dans la main avec les fichiers statiques qui ne marchent pas, les URL qui ne matchent pas et les CBV qui nheuuuu, juste pas.

Une fois qu’on a compris le principe, ça va pour la prog normal, mais un jour on a besoin de faire un POST en Ajax, et on se retrouve avec une erreur invisible. Après avoir dégainé Firebug, on comprend qu’on a une 403 forbidden, et votre cerveau finit (la durée galérienne est plus ou moins longue selon les profiles, les heures de sommeil et les phases de la lune) par réaliser qu’on n’a pas envoyé le token CSRF. Merde.

C’est là que généralement les gens sortent du @csrf_exempt, ou carrément, en finissent avec cette solution radicale :

MIDDLEWARE_CLASSES = (
    'django.middleware.gzip.GZipMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
)

Mais c’est quand même dommage d’en arriver là alors qu’on peut le faire proprement.

D’abord, le token est sauvegardé dans un cookie. Il faut donc le récupérer.

// ue petite fonction pour récupérer la valeur d'un cookie,
// puisque bien entendu, comme toutes les APIS javascripts,
// les choses qu'on fait le plus souvent ne sont pas incluses
// en natif. Oui je suis aigri.
function getCookie(name) {
    if (document.cookie && document.cookie != '') {
        var cookies = document.cookie.split(';');
        for (var i = 0; i < cookies.length; i++) {
            var cookie = jQuery.trim(cookies[i]);
            if (cookie.substring(0, name.length + 1) == (name + '=')) {
                return decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
}

Sinon, pour les fainéants, il y a y a un plugin jquery qui permet de faire $.cookie('nom').

Ensuite, on attache cette valeur en header avec toutes les requêtes POST. Comme ça, plus besoin de l’inclure manuellement, on peut faire ses requêtes sans y penser.

Avec jQuery :

$.ajaxSetup({
    // fonction appelée avant d'envoyer une requête AJAX
    beforeSend: function(xhr, settings) {
         // on ajoute le header que si la requête est pour le site en cours
         // (URL relative) et est de type POST
         if (!/^https?:.*/.test(settings.url)  && settings.type == "POST") {
             // attachement du token dans le header
             xhr.setRequestHeader("X-CSRFToken",  getCookie('csrftoken'));
         }
     }
});

Avec AngularJs :

// interception de la configuration du provider HTTP
// qui possède un mécanisme déjà tout prêt pour ça
votre_app.config(function($httpProvider) {
    $httpProvider.defaults.xsrfCookieName = 'csrftoken';
    $httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
});

Attention, si l’app n’est pas servie par Django (template statique avec uniquement des appels Ajax), il faut faire au moins un GET avant de faire son premier POST afin d’obtenir le cookie.

19 thoughts on “La protection CSRF de Django et les requêtes Ajax

  • golgotha

    Je trouve que c’est assez simple sinon de passer le token dans les data.

    $.ajax({
    data: {
    csrfmiddlewaretoken: '{{ csrf_token }}'
    }

  • Sam Post author

    Oui mais :

    – il faut le faire à chaque fois. Sur une grosse app, ça fait des dizaines de copier/coller.
    – faut utiliser $.ajax au lieu de $.post, qui est fort pratique.

  • François Gilbert

    Très intéressant, merci !
    Je testerai ça mais je pense que ça va résoudre un petit problème que j’ai avec une fonction ajax rafraichissant une portion de page qui, au bout de 10 minutes, n’affiche plus qu’une page de login (à cause d’une session utilisateur expirée).

    Je profite de ce message pour vous remercier pour tout le boulot que vous avez fait sur ce site concernant Django. J’ai réalisé mon stage de fin d’études avec ce framework en ne connaissant absolument rien en HTML et Python, et vous avez votre place pour les sources dans mon mémoire :)

  • pouete

    Et ça marches pas forcement si tu veux eviter les jajascripts dans ton template.
    Ensuite, on peut aussi faire le gros degueulasse et entourer le token avec un champ input hidden et le recuperer ensuite dans le requete ajax.

  • Paul

    Heu j’ai une question bête:
    Le token CSRF est sauvegardé dans les cookies?
    Quand on fait un appel Ajax en GET ou POST, ca n’est pas censé envoyer les cookies du domaine avec?

  • Sam Post author

    @Marc: lib très bien faite, au passage, et qui ne pèse que 3ko minifiée. Ça gère les reverse URL et tout le bordel.

  • Sam Post author

    @Paul : normalement c’est le cas, je suis incapable de t’expliquer pourquoi ça ne marche pas out of the box.

  • Paul

    @JEDI_BC, ce que je ne comprend toujours pas (dsl je me sens bête), c’est que:
    1) Un cookie devrait être envoyé par défaut dès qu’on fait appel à un url du même domaine du cookie, Ajax ou pas. Donc je suis étonné et ne pige pas vraiment qu’il faille le rajouter à la main.
    2) Mais j’ai compris qu’il faut d’une certaine manière “accéder à ce cookie spécial manuellement”, ce qui suppose que le JS est sur le bon nom de domaine, pour envoyer ce cookie crypté. Ce qui ne peut être fait à partir d’un autre site. Sinon cela serait complètement inutile car il suffirait simplement d’envoyer une requête ajax depuis un site pirate pour effectuer notre opération admin à son insu (puisque les cookies du domaine sont envoyés avec l’appel à ce domaine en ajax) – cf les pubs qui bardent de cookies partout et nous trackent.

  • Sam Post author

    @JEDI_BC: ahhhhhhhhhhhhhhh ok. Merci mec !

    @Paul: en fait, ce que JEDI veut dire, c’est que le token doit être présent à la fois dans le cookie ET dans un autre medium pour que le CSRF soit pris en compte. Le cookie ne suffit pas. Si on utilise un formulaire, on envoit le cookie automatiquement, mais il faut aussi le mettre dans un champ input caché. Si on envoie une requête Ajax, on envoit le cookie, mais il faut aussi le foutre dans le header.

  • JEDI_BC

    @Paul : le principe est de vérifier la correspondance du token dans le cookie et du token dans le form ou header (d’où le double dans double submit validation). Une attaque par csrf ne peut pas altérer les 2. On peut aussi ajouter une troisième vérification avec le token en session si on gère une session bien sur.

  • Paul

    @Sam @JEDI_BC Ok l’idée est donc de le passer dans le header dans un champ spécial donc qui est différent d’une champ de cookie (la version Angular est confusing! Mais je vois avec la version Jquery). Merci!!

  • geekingfrog

    Si ça marchait out of the box avec le csrf dans les cookies, ça n’aurait aucune utilitée. Dans ce cas, si l’attaquant te leure pour que tu POST quelque chose sur le domaine visée, ton browser va envoyer les cookies avec. L’idée du token c’est de rajouter une donnée que l’attaquant ne peux pas deviner, donc la requete forgée va échouer.

    En bref: les attaques csrf utilisent les cookies pour bypass l’authentication, donc il faut autre chose (un header typiquement).

    @Jedi une troisième vérification sur la session??? Si la session est stockée dans un cookie, ça ne sert à rien non plus. Je suis pas sûr de comprendre là.

  • Sam Post author

    Du coup, pourquoi faire un token en plus ? Pourquoi ne pas juste utiliser l’id de session.

  • JEDI_BC

    @Sam : un token doit être utilisé pour une seule requête et une seule et regénéré ensuite (renvoyé par cookie du retour http)

    @geekingfrog : seul l’id de session est stocké dans le cookie (enfin en PHP :p), les informations de la session comprenant le token sont elles stockées sur le serveur. Le but est de mixer le Synchronizer Token Pattern et le Double Submit Pattern pour renforcer encore plus la sécurité (voir mon lien précédent).

  • Sam Post author

    Oui, logique, sinon un mec qui écoute sur la ligne peut récup le truc et le réutiliser.

  • loon

    Ce n’est pas géré par défault !

    Je ne connais pas vraiment Django.
    Mais sur Laravel on peu ajouter un jeton csrf sur toutes les routes en post par exemple, en une seule ligne dans le fichier de routing.

    Ça doit sûrement exister sur Django.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.