L’idée, ici, est de présenter l’usage d’un « token » (jeton) pour se prémunir d’une attaque CSRF (Cross Site Request Forgery) en utilisant le framework Symfony 4+ (doit pouvoir fonctionner en Sf 3 également à peu de chose près) mais sans s’appuyer sur un formulaire.
En effet les tokens CSRF sont proposés pour la sécurité d’un formulaire Symfony, donc l’objectif de ma démarche avec ce « bout de code » est de mettre en place la même sécurité, en utilisant un simple lien ou un bouton HTML par exemple avec une requête « DELETE » en AJAX pour appeler l’action dédiée « DeleteMemberAction » ci-dessous.
Note : utiliser une requête en « GET » pour une suppression de ressource est une mauvaise pratique d’un point de vue HTTP.
Cet exemple tient compte du fait que l’utilisateur à l’origine de l’action est normalement authentifié sur un espace sécurisé, ce qui fait le danger par principe d’une attaque CSRF.
Un compte « membre » doit être supprimé, ce que l’on peut considérer comme une opération sensible !
<?php declare(strict_types = 1); namespace App\Action; use App\Responder\DeleteMemberResponder; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; /** * Class DeleteMemberAction. * * Delete a member account. */ class DeleteMemberAction { /** * Manage member deletion. * * @Route({ * "en": "/user/{id}/delete/{token}" * }, name="delete_member", methods={"DELETE"}) * * @param CsrfTokenManagerInterface $csrfTokenManager * @param DeleteMemberResponder $responder * @param Request $request * * @return Response * * @throws InvalidCsrfTokenException */ public function __invoke(CsrfTokenManagerInterface $csrfTokenManager, DeleteMemberResponder $responder, Request $request): Response { // "delete_member" must be a unique token id for session storage inside application $token = new CsrfToken('delete_member', $request->attributes->get('token')); // Action is stopped since token is not allowed! if (!$csrfTokenManager->isTokenValid($token)) { throw new InvalidCsrfTokenException('CSRF Token is not valid.'); } // Handle request and proceed to parameters validation and member deletion // ... // Return expected response (e.g. JSON, HTML...) } }
Note : cette action est auto-configurée en tant que « controller » au sein du framework pour être appelée correctement via la route précisée.
Ce « controller » va en outre enclencher la vérification de la validité du token par l’intermédiaire d’un service « CsrfTokenManager » implémentant une interface. Vous trouverez ici le détail de cette classe concrète.
Cet objet « CsrfTokenManager » vérifie la validité d’un autre objet « CsrfToken » associant un identifiant et une valeur de token.
Mais à ce propos comment le token est généré ?
Généralement, il est créé dans un template via une fonction Twig avec « csrf_token(‘delete_member’) » qui permet d’avoir le retour de la valeur du token CSRF en passant l’identifiant correspondant attendu.
Voici ce que cela peut donner sur un simple lien HTML côté Twig (l’allié fidèle de Symfony) :
{# "id" parameter can also be a uuid value! #} <a href="{{ path('delete_member', {'id': 450, 'token': csrf_token('delete_member')}) }}" title="Validate deletion">Delete account</a>
Bien entendu, il est préférable par exemple de demander une confirmation avant suppression, afin d’être plus user friendly !
Le token CSRF peut être généré aussi simplement dans un service, toujours grâce à l’instance « CsrfTokenManager » et sa méthode « getToken(‘delete_member’) » qui retourne un objet « CsrfToken » et va générer/stocker le token s’il n’existe pas déjà.
L’objet « CsrfToken » a quant à lui une méthode « getValue() » afin de récupérer en définitive la valeur du token.
{# Strange getter which can also generate a token! #} $token = $csrfTokenManager->getToken('delete_member')->getValue();
Ce manager fait appel à d’autres instances pour générer le token et le stocker en session notamment via les classes concrètes « UriSafeTokenGenerator » et « NativeSessionTokenStorage » ou des implémentations similaires.
J’espère que ces quelques lignes pourront vous rendre service, sans mauvais jeu de mot !