Comprendre et éviter les attaques CSRF grâce à Symfony
CSRF veut dire Cross-Site Request Forgery en anglais, une traduction française pourrait être « Falsification de requêtes inter-sites ».
Dans cet article, nous allons faire un rappel de ce qu’est une attaque CSRF et comment Symfony nous en protège. Puis au travers d’un exemple concret, voir qu’il reste des cas qui sortent du cadre classique et méritent toute notre attention.
Section intitulée qu-est-ce-qu-une-attaque-csrfQu’est-ce qu’une attaque CSRF ?
L’objet de cette attaque est de transmettre à un utilisateur authentifié une requête HTTP falsifiée qui pointe sur une action interne au site, afin qu’il l’exécute sans en avoir conscience et en utilisant ses propres droits. L’utilisateur devient donc complice d’une attaque sans même s’en rendre compte. L’attaque étant actionnée par l’utilisateur, un grand nombre de systèmes d’authentification sont contournés. Source : Wikipédia
Concrètement l’attaque se passe en trois temps :
- Alice est administratrice d’un forum, elle a une session active.
- Bob forge une URL
/admin/delete/comment/345
, l’obstrue par exemple avec un raccourcisseur d’URL, puis l’envoie à Alice - Alice clique sur l’URL, ayant une session active, l’action destructrice est exécutée malgré elle
Heureusement, Symfony est bien fait et grâce au package symfony/security-csrf
(ou via le composant Form
qui l’intègre), le framework s’occupe de tout !
Il est cependant intéressant de s’y re-pencher un peu pour éviter les quelques pièges restants.
Section intitulée formulairesFormulaires
Quand vous utilisez un FormType
et que l’option CSRF est activée dans la configuration (ce qui est le cas par défaut), vous êtes protégé.
# config/packages/framework.yaml
framework:
csrf_protection: true
Section intitulée c-est-magiqueC’est magique
En effet, Symfony est assez intelligent pour ajouter un champ caché dans votre formulaire au moment où vous l’utilisez dans votre application.
Form
fonctionne parfaitement avec le moteur de template Twig :
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_end(form) }}
^-- L'objet formView est rendu
et ajoute un champ caché avec le token
Concrètement, c’est ce bout de code qui sera automatiquement ajouté pour vous :
<form>
...
<input type="hidden" id="form_name__token"
name="form_name[_token]"
value="...87deab327835c5e20c17964c41b3b854...">
</form>
Ainsi, quand dans votre contrôleur, vous faites votre appel au formulaire et à sa validité, le CSRF est vérifié dans la foulée. Magie !
if ($form->isSubmitted() && $form->isValid()) { ... }
Section intitulée la-magie-a-un-prixLa magie a un prix
Les tokens CSRF utilisés sont différents pour chaque utilisateur et sont stockés dans la session (en plus d’être ajoutés au formulaire donc) afin d’être comparés. Une session est donc automatiquement démarrée dès que vous créez un formulaire avec une protection CSRF.
Cela signifie concrètement que vous ne pouvez plus mettre en cache ces pages protégées contre les attaques CSRF !
Il existe des solutions, comme l’utilisation des caches partiels, ou l’injection du token CSRF en Ajax, mais elles dépassent le cadre de cet article. Plus d’informations en anglais dans la documentation officielle de Symfony.
Note : Il peut arriver que vous deviez gérer un formulaire manuellement ; à ce moment-là, n’oubliez pas qu’il est nécessaire de vous occuper du CSRF vous-même. Nous en verrons un exemple plus loin dans l’article.
Section intitulée quelques-cas-restent-a-votre-chargeQuelques cas restent à votre charge
Pour protéger les actions destructrices dans vos contrôleurs, il est important de bien faire attention à la sécurité. Mais, il est possible que certaines fonctionnalités de Symfony portent à confusion sur l’étendue de la sécurisation qu’elles permettent.
Dans ce scénario, une nouvelle personne développe pour la première fois sur Symfony. Cette personne a compris que Symfony gère automatiquement les problématiques de CSRF et protège donc son contrôleur grâce à un simple IsGranted
.
Mais ça n’est pas suffisant !
Section intitulée protege-par-code-isgranted-codeProtégé par IsGranted
❌
#[IsGranted('ROLE_ADMIN')]
class AdminController extends AbstractController
{
// ...
#[Route(path: '/admin/comment/{id}/delete', name: 'admin_comment_delete')]
public function commentDelete(Comment $comment, CommentRepository $commentRepository): Response
{
$commentRepository->remove($comment, flush: true);
return $this->redirectToRoute('admin_comment_list');
}
}
Mon contrôleur est protégé par un #[IsGranted('ROLE_ADMIN')]
, c’est donc sécurisé !
Eh bien non, une attaque de type CSRF est possible via un lien piégé envoyé à l’administrateur. Par exemple, l’attaquant a repéré le commentaire avec l’id 345
et il veut le supprimer. Il n’est pas administrateur, mais il peut créer un lien vers https://example.org/admin/comment/345/delete
, ce lien il va le cacher derrière un raccourcisseur d’URL par exemple : https://tinyurl.com/funny-cat-playing-piano
et maintenant il suffit d’envoyer ce lien à un administrateur et qu’il clique dessus. En cliquant dessus, étant déjà identifié sur le site cible, l’administrateur lancera malgré lui l’action cachée derrière l’URL. C’est un exemple grossier, mais l’idée est là.
Section intitulée protege-par-code-methods-post-codeProtégé par methods: ['POST']
❌
#[IsGranted('ROLE_ADMIN')]
class AdminController extends AbstractController
{
// ...
#[Route(
path: '/admin/comment/{id}/delete',
name: 'admin_comment_delete',
methods: ['POST'])
]
public function commentDelete(Comment $comment, CommentRepository $commentRepository): Response
{
$commentRepository->remove($comment, flush: true);
return $this->redirectToRoute('admin_comment_list');
}
}
couplé à :
<form action="{{ path('admin_comment_delete', {id: comment.id}) }}" method="post">
<button>Delete Comment</button>
</form>
Mon action est protégée, car l’attaquant ne peut pas forger un lien, c’est désormais un formulaire et la méthode doit être un POST.
Eh bien, encore non, une attaque de type CSRF est toujours possible, bien que plus compliquée. L’attaquant peut créer un formulaire POST qui pointe sur l’URL https://example.org/admin/comment/345/delete
. Avec un peu de JavaScript ce formulaire peut être auto-soumis au chargement de la page :
<form action="https://example.org/admin/comment/345/delete" method="POST"></form>
<script>document.forms[0].submit()</script>
Une fois la page hébergée, il suffit d’envoyer le lien vers cette page, ou un lien caché. Le problème reste le même.
Section intitulée protege-par-un-token-csrfProtégé par un token CSRF ✅
#[IsGranted('ROLE_ADMIN')]
class AdminController extends AbstractController
{
// ...
#[Route(
path: '/admin/comment/{id}/delete',
name: 'admin_comment_delete',
methods: ['POST'])
]
public function commentDelete(Request $request, Comment $comment, CommentRepository $commentRepository): Response
{
if (
!$this->isCsrfTokenValid(
'delete' . $comment->getId(),
$request->request->get('_token')
)) {
throw new BadRequestHttpException();
}
$commentRepository->remove($comment, flush: true);
return $this->redirectToRoute('admin_comment_list');
}
}
couplé à :
<form action="{{ url('admin_comment_delete', {id: comment.id}) }}" method="post">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ comment.id) }}"/>
<button>Delete Comment</button>
</form>
Détail sur la méthode : isCsrfTokenValid(string $id, ?string $token): bool
Le but du paramètre $id
est de créer un token le plus unique possible, vous pouvez le voir comme un namespace. Dans notre exemple, nous avons créé un token basé sur un mot “delete” concaténé à l’id de l’objet que nous souhaitons supprimer. Ces deux informations étant disponibles dans le template et dans notre action, il est donc possible d’en faire un namespace et de sécuriser encore plus l’ensemble.
Le second paramètre $token
est tout simplement le token envoyé par le formulaire dans la requête.
La méthode s’occupe ensuite de valider l’ensemble.
Cette fois-ci c’est un succès, l’action est protégée des attaques CSRF. Symfony s’occupe de la génération et validation du token, qui change à chaque requête. L’attaquant ne peut pas générer un token valide.
Section intitulée aller-plus-loin-dans-la-securiteAller plus loin dans la sécurité
Il existe une variante de l’attaque CSRF qui vise spécifiquement les formulaires de login. L’idée n’est pas ici de forcer la personne à réaliser une action avec ses droits, mais à l’inverse de la connecter avec les identifiants de l’attaquant afin de lui faire réaliser des actions pour le compte de l’attaquant : exemple, payer une commande. Symfony dispose d’une configuration spécifique pour se prémunir de ces attaques : documentation officielle > Sécurité > Form login
Évidemment, la protection CSRF n’est qu’un seul des nombreux aspects de sécurité autour des formulaires et plus généralement des applications Web.
Par exemple Laurent Brunet nous parle du CSP (Content Security Policy) mais on pourrait aussi citer CORS qui est complémentaire.
Section intitulée conclusionConclusion
Sécuriser une application est souvent compliqué, mais heureusement Symfony nous donne accès à beaucoup d’outils. Cela n’empêche pas un oubli de la part de la personne qui implémente.
Bien sûr, il existe des aide-mémoire récapitulant tous les points de vigilance. Une de ces listes peut être retrouvée sur Open Worldwide Application Security Project.
C’est sans doute une bonne pratique de vérifier l’ensemble des points avant de livrer un projet !
Commentaires et discussions
Using Symfony Form in WordPress
What a strange idea! Once upon a time, a developer was asked to move a form from one application to another. The source application was a Symfony app. The target application was WordPress, the CMS that runs the Web. Follow us in that journey that will take you to the edge of what…
Des champs de formulaire Symfony sécurisés par vos données avec Symfony
Dans le chapitre précédent, nous avons vu comment ajouter de la sécurité sur un champs de formulaire en fonction des rôles de l’utilisateur. Cependant la gestion de la sécurité peut être plus complexe. En effet, il est commun d’avoir besoin de l’objet (Article dans notre exemple)…
Lire la suite de l’article Des champs de formulaire Symfony sécurisés par vos données avec Symfony
Nos articles sur le même sujet
Nos formations sur ce sujet
Notre expertise est aussi disponible sous forme de formations professionnelles !
Symfony
Formez-vous à Symfony, l’un des frameworks Web PHP les complet au monde
Ces clients ont profité de notre expertise
À l’occasion de la 12e édition du concours Europan Europe, JoliCode a conçu la plateforme technique du concours. Ce site permet la présentation des différents sites pour lesquels il y a un appel à projets, et encadre le processus de recueil des projets soumis par des milliers d’architectes candidats. L’application gère également toute la partie post-concours…
La refonte de la plateforme odealarose.com par JoliCode a représenté une transformation complète de tous les aspects de l’application. En charge de l’aspect technique, nous avons collaboré avec Digital Ping Pong (société cofondée par JoliCode), qui a eu en charge à la conception en revisitant entièrement le parcours client et l’esthétique de la plateforme…
Ouibus a pour ambition de devenir la référence du transport en bus longue distance. Dans cette optique, les enjeux à venir de la compagnie sont nombreux (vente multi-produit, agrandissement du réseau, diminution du time-to-market, amélioration de l’expérience et de la satisfaction client) et ont des conséquences sur la structuration de la nouvelle…