La falsification de requête est une attaque discrète : un visiteur connecté à votre site consulte une autre page qui déclenche, à son insu, une requête vers votre application. Le navigateur joint automatiquement les témoins de session, et le serveur exécute l'action comme si l'utilisateur l'avait demandée. Changement d'adresse électronique, suppression de contenu, virement dans les cas les plus graves. La protection tient en un jeton anti-CSRF, valeur imprévisible connue du serveur et transmise avec chaque formulaire, dont l'absence ou l'invalidité provoque un rejet. Le principe est simple, l'implémentation l'est moins, et une protection mal écrite donne un faux sentiment de sécurité qui vaut souvent moins que pas de protection du tout.
Comprendre ce que le jeton protège
La compréhension du mécanisme d'attaque détermine directement la façon d'écrire la protection, et l'expérience montre qu'une implémentation faite sans cette compréhension comporte presque toujours une faille. Le sujet s'inscrit dans le panorama que nous dressons à propos des failles de sécurité d'un site Internet.
Le mécanisme de l'attaque
Un site tiers contient un formulaire ou une image dont l'adresse pointe vers votre application. Lorsqu'un utilisateur connecté visite cette page, son navigateur envoie la requête en y joignant automatiquement les témoins associés à votre domaine. Le serveur voit une requête parfaitement authentifiée et l'exécute. L'attaquant ne lit jamais la réponse, ce qui limite son intérêt aux actions ayant un effet, mais ces actions suffisent largement à causer des dégâts. Il n'a pas besoin de connaître le mot de passe ni de voler quoi que ce soit, il lui suffit que la victime soit connectée. C'est ce qui rend l'attaque particulièrement adaptée aux interfaces d'administration, où les personnes restent connectées de longues heures durant.
Pourquoi la session ne suffit pas
Le témoin de session prouve que la requête vient du navigateur de l'utilisateur, il ne prouve pas qu'elle vient d'une page de votre site. C'est précisément la distinction que le jeton établit : sa valeur n'est connue que des pages que vous avez servies, et un site tiers ne peut pas la deviner ni la lire, la politique d'origine des navigateurs l'en empêchant. Cette distinction entre authentification et intention est le cœur du sujet, et elle explique pourquoi aucune vérification d'identité, si robuste soit elle, ne remplace le jeton. Une authentification à deux facteurs impeccable laisse le site tout aussi vulnérable si les actions ne sont pas protégées par un jeton.
Les actions à protéger
Toute requête produisant un effet doit être protégée : création, modification, suppression, envoi de message, changement de paramètre. Les requêtes de simple lecture n'ont pas besoin de jeton, à condition qu'elles soient effectivement sans effet. C'est là qu'un défaut de conception classique se révèle : une action de suppression accessible par un lien ordinaire est vulnérable par nature, puisqu'une simple image placée sur un site tiers suffit à la déclencher. Le premier travail consiste donc souvent à convertir ces liens en formulaires. Cette conversion présente un autre avantage : elle empêche les robots d'exploration de déclencher accidentellement ces actions, incident qui se produit régulièrement sur les administrations mal protégées.
Ce que le jeton ne protège pas
Un jeton ne protège pas contre l'injection de script dans vos propres pages : un script exécuté dans le contexte de votre site peut lire le jeton et l'utiliser. Les deux protections sont donc complémentaires, et une faille d'injection annule entièrement l'intérêt du jeton. Une politique de sécurité du contenu correctement configurée réduit ce risque, comme nous l'expliquons dans notre article consacré à la Content Security Policy. Il faut retenir que la protection contre les injections reste prioritaire, une faille de ce type rendant toutes les autres protections inopérantes.
Les attributs de témoin
L'attribut limitant l'envoi des témoins aux requêtes provenant du même site constitue une protection complémentaire très efficace, désormais appliquée par défaut par les navigateurs dans sa forme intermédiaire. Il ne dispense pas du jeton pour autant : la valeur par défaut laisse passer certaines navigations, la protection dépend entièrement du navigateur utilisé, et un navigateur ancien ne l'applique pas. La bonne pratique consiste à activer explicitement cet attribut et à conserver le jeton, les deux mécanismes se renforçant mutuellement. La valeur stricte de cet attribut est préférable partout où elle ne casse pas de parcours légitime, notamment sur les témoins d'administration.

Générer et stocker le jeton
La génération et le stockage déterminent la solidité réelle de la protection. Trois erreurs suffisent à la rendre entièrement inopérante tout en donnant l'impression qu'elle fonctionne.
Une source aléatoire cryptographique
Le jeton doit être produit par un générateur adapté à la cryptographie, jamais par les fonctions de hasard ordinaires du langage, dont les valeurs sont prévisibles à partir de quelques observations. PHP propose une fonction dédiée qui retourne des octets réellement imprévisibles, à convertir ensuite en représentation hexadécimale pour le transport. Une longueur de trente deux octets est largement suffisante et ne coûte rien. Utiliser un identifiant de session, un horodatage ou une empreinte de l'identifiant utilisateur comme jeton revient à ne rien protéger, ces valeurs étant devinables ou déductibles. Le contrôle est simple : si deux utilisateurs différents peuvent obtenir la même valeur, ou si la même valeur revient d'une session à l'autre, le jeton ne vaut rien.
Le stockage côté serveur
Le jeton doit être conservé dans la session, ce qui garantit qu'il reste hors de portée du client et des sites tiers. La session étant déjà nécessaire pour l'authentification, ce stockage ne coûte rien de plus. Il faut simplement s'assurer que la session est démarrée avant toute génération, sous peine d'un jeton perdu entre la production de la page et sa soumission. C'est une source d'erreur fréquente sur les applications où le démarrage de session est conditionnel. Une fonction unique chargée de démarrer la session si nécessaire puis de retourner le jeton supprime définitivement ce genre d'incident.
Un jeton par session ou par formulaire
Deux stratégies coexistent. Un jeton unique par session est simple, suffisant dans la plupart des cas et compatible avec l'ouverture de plusieurs onglets. Un jeton par formulaire offre une protection légèrement supérieure et complique sérieusement la vie de l'utilisateur, qui verra ses soumissions rejetées dès qu'il travaille dans plusieurs onglets. Pour une application ordinaire, le jeton par session est le bon compromis ; le jeton par formulaire se justifie sur les actions vraiment sensibles, à condition d'en assumer les conséquences ergonomiques. Une solution intermédiaire consiste à conserver une réserve de plusieurs jetons valides simultanément, ce qui préserve le multi onglet tout en limitant la durée de validité de chacun.
La durée de vie
Un jeton lié à la session expire avec elle, ce qui est cohérent. Sur les formulaires longs, remplis en trente minutes, une expiration de session en cours de saisie provoque un rejet et la perte du contenu, expérience détestable. La parade consiste à conserver la saisie et à proposer une reconnexion sans perdre les données, plutôt qu'à allonger indéfiniment la durée de session. Cette solution demande un peu de travail et évite les abandons sur les formulaires de commande, où l'enjeu est réel. Un avertissement affiché quelques minutes avant l'expiration, avec une possibilité de prolonger la session, complète utilement ce dispositif.
La transmission dans le formulaire
Le jeton se transmet dans un champ caché du formulaire, ce qui est parfaitement sûr : le caractère caché ne sert pas à le dissimuler mais à éviter de l'afficher. Ce qui protège n'est pas le secret du champ mais l'impossibilité pour un site tiers de lire sa valeur. Il faut évidemment l'échapper au moment de l'écriture dans l'attribut, réflexe que l'on applique de toute façon à toute valeur écrite dans une page, comme nous le rappelons dans notre article sur la manière de valider une saisie côté serveur. Le nom du champ n'a en revanche aucune importance pour la sécurité, et il peut parfaitement rester explicite.
Ne jamais le mettre dans une adresse
Un jeton placé dans les paramètres d'une adresse se retrouve dans les journaux du serveur, dans l'historique du navigateur, dans l'en-tête de référent transmis aux sites tiers et dans les liens que les utilisateurs se partagent. Chacune de ces fuites suffit à compromettre la protection. La règle est donc absolue : le jeton voyage dans le corps de la requête ou dans un en-tête, jamais dans l'adresse, quelle que soit la commodité que cela représenterait pour le développement. Cette règle vaut également pour les jetons d'invitation et de réinitialisation de mot de passe, qui souffrent exactement des mêmes fuites.
| Point | Bonne pratique | Erreur fréquente |
|---|---|---|
| Génération | Générateur cryptographique | Fonction de hasard ordinaire |
| Longueur | 32 octets | Valeur courte ou devinable |
| Stockage | Session côté serveur | Champ caché seul |
| Transmission | Corps de requête ou en-tête | Paramètre d'adresse |
| Comparaison | Comparaison à temps constant | Comparaison simple |
| Portée | Toutes les actions à effet | Formulaires visibles seulement |
| Échec | Rejet et message clair | Poursuite du traitement |
Vérifier correctement
La vérification paraît triviale et concentre pourtant plusieurs pièges, dont certains transforment une protection apparente en absence totale de protection.
Comparer à temps constant
Une comparaison de chaînes ordinaire s'arrête au premier caractère différent, ce qui rend son temps d'exécution dépendant du nombre de caractères communs. Un attaquant patient peut exploiter cette différence pour deviner le jeton caractère par caractère. La fonction de comparaison à temps constant fournie par le langage supprime ce risque et s'utilise exactement comme une comparaison ordinaire. Il n'y a aucune raison de ne pas l'employer, et son absence est un signal de code écrit sans considération pour la sécurité. Elle attend deux chaînes de même longueur, condition remplie par construction lorsque les jetons sont générés correctement.
Vérifier avant tout traitement
Le contrôle doit intervenir en tout premier, avant la validation des champs, avant toute écriture et avant tout appel externe. Une vérification placée après le traitement ne protège rien, situation que l'on rencontre plus souvent qu'on ne le croit sur du code écrit par ajouts successifs. Le bon emplacement est une fonction appelée en première ligne du traitement de toute requête modifiant l'état, idéalement par un mécanisme centralisé plutôt que répété dans chaque contrôleur. Un contrôle répété manuellement finit toujours par être oublié sur une action ajoutée dans l'urgence.
Rejeter sans ambiguïté
Un jeton absent ou invalide doit provoquer un arrêt immédiat avec un code de réponse approprié, et non un simple message ajouté à une liste d'erreurs qui laisserait le traitement se poursuivre. Le message affiché à l'utilisateur doit rester générique, en invitant à recharger la page, sans détailler la nature du problème. Une explication trop précise renseigne inutilement, et de toute façon aucun utilisateur légitime n'a besoin de savoir ce qu'est un jeton. Un rechargement de page suffit dans l'immense majorité des cas légitimes, généralement une session expirée ou un onglet resté ouvert trop longtemps.
Le cas des appels asynchrones
Les requêtes émises par le code du navigateur doivent transmettre le jeton dans un en-tête dédié, valeur récupérée depuis un attribut du document ou depuis une variable injectée dans la page. La vérification côté serveur lit alors cet en-tête plutôt qu'un champ de formulaire. Il faut prévoir les deux modes de lecture dans la fonction de vérification, faute de quoi une partie des actions échappe au contrôle, généralement celles qui ont été ajoutées après la mise en place initiale. Les bibliothèques de requêtes du navigateur permettent de configurer cet en-tête une fois pour toutes, ce qui évite de le répéter à chaque appel.
Les interfaces de programmation
Une interface authentifiée par un jeton porté dans un en-tête, et non par un témoin de session, n'est pas vulnérable à cette attaque, puisque le navigateur ne joint pas automatiquement ce type d'identifiant. La protection y est donc inutile et son ajout complique la vie des intégrateurs sans rien apporter. Distinguer clairement les points d'entrée authentifiés par session de ceux authentifiés par jeton applicatif évite beaucoup de discussions inutiles sur ce sujet. Une interface acceptant les deux modes d'authentification doit en revanche appliquer la protection lorsque la requête est authentifiée par session.
Gérer les onglets multiples
Un utilisateur ouvrant plusieurs onglets sur votre application est un cas normal et fréquent qui doit fonctionner sans accroc. Un jeton par session le gère naturellement. Un jeton régénéré à chaque page invalide les onglets précédemment ouverts et produit des rejets incompréhensibles pour l'utilisateur, qui conclut que le site est cassé. Ce compromis entre sécurité et confort doit être tranché consciemment, en connaissant le comportement réel des utilisateurs de l'application.
Défauts constatés lors de revues d'applications web. Les deux premiers laissent des actions entièrement exposées malgré la présence d'un dispositif de protection.
Mettre en place et tester
Une protection qui n'est pas appliquée partout ne protège rien, ce qui fait de la systématicité l'enjeu principal de la mise en œuvre.
Centraliser la vérification
Le contrôle doit être appliqué par un mécanisme unique, exécuté avant tout traitement de requête modifiant l'état, plutôt que par un appel répété dans chaque contrôleur. Cette centralisation garantit qu'une nouvelle action ajoutée dans six mois sera protégée par défaut. Elle suppose de disposer d'un point de passage commun, ce que la plupart des architectures offrent, et son absence explique la majorité des oublis constatés en audit.
Utiliser le mécanisme du cadre applicatif
Les cadres applicatifs et les systèmes de gestion de contenu fournissent tous un mécanisme de jeton, avec des noms différents mais un fonctionnement identique. L'utiliser plutôt que de réécrire le sien garantit une implémentation correcte, testée et maintenue. Réécrire cette brique est un exercice pédagogique intéressant et un mauvais choix en production, la moindre erreur produisant une protection illusoire que rien ne signale.
Recenser les points d'entrée
Un inventaire de toutes les actions modifiant l'état, formulaires, appels asynchrones, liens d'action, permet de vérifier que chacune est protégée. Cet inventaire se construit une fois et se relit à chaque évolution significative. Il révèle presque toujours quelques actions oubliées, généralement dans l'administration, où l'on suppose à tort que l'accès restreint suffit à protéger, alors que c'est justement là que les actions sont les plus sensibles.
Tester l'absence de jeton
Le test consiste à envoyer une requête sans jeton, puis avec un jeton invalide, et à vérifier dans les deux cas que rien ne se produit côté serveur. Ce test se scripte facilement et doit couvrir chaque type d'action. C'est le seul moyen de s'assurer que la protection est réellement active, une lecture du code ne suffisant pas à garantir qu'un chemin détourné n'existe pas.
Vérifier depuis un autre site
Le test le plus proche de la réalité consiste à construire une page hébergée ailleurs, contenant un formulaire pointant vers votre application, et à la soumettre en étant connecté. Si rien ne se passe, la protection fonctionne. Cette manipulation prend dix minutes et emporte une conviction que ne procure aucune relecture de code, ce qui la rend particulièrement utile pour convaincre une équipe de l'utilité de l'effort.
Surveiller les rejets
Les rejets pour jeton invalide doivent être journalisés. Un volume anormal signale soit une attaque en cours, soit un défaut d'implémentation gênant les utilisateurs légitimes, deux situations qu'il vaut mieux connaître. Distinguer ces deux causes se fait en regardant l'origine des requêtes rejetées et leur répartition dans le temps, analyse rapide qui évite de laisser un problème d'ergonomie se prolonger pendant des mois.
💬 0 commentaires
✍️ Laisser un commentaire
Créez un compte gratuit ou connectez-vous pour commenter.