Backend

Comprendre et se protéger des attaques 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

5 min de lecture
Comprendre et se protéger des attaques CSRF

La falsification de requêtes inter-sites, également connue sous le nom d'attaque en un clic ou de session riding et abrégée en CSRF ou XSRF, est un type d'exploitation malveillante d'un site web où des commandes non autorisées sont transmises par un utilisateur auquel l'application web fait confiance. Un site web malveillant peut transmettre de telles commandes de nombreuses manières ; des balises d'image spécialement conçues, des formulaires cachés et des requêtes JavaScript XMLHttp (AJAX), par exemple, peuvent tous fonctionner sans que l'utilisateur n'ait à intervenir ou même à son insu. Contrairement au cross-site scripting (XSS), qui exploite la confiance qu'un utilisateur a pour un site particulier, le CSRF exploite la confiance qu'un site a dans le navigateur d'un utilisateur.

Objectif d'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.

Protection dans une application PHP

Aujourd'hui avec le PSR, sécuriser une application PHP contre les attaques CSRF est relativement facile, en effet il existe plusieurs libraires pour nous faciliter la tâche. voici un exemple :

$ composer require grafikart/psr15-csrf-middleware

pour utiliser cette librairie :

<?php
$middleware = new CsrfMiddleware($_SESSION, 200);
$app->pipe($middleware);
// Generate input
$input = "<input type=\"hidden\" name=\"{$middleware->getFormKey()}\" value=\"{$middleware->generateToken()}\"/>

Ce middleware PSR-15 vérifie toutes les requêtes de type POST, PATCH, PUT et DELETE pour un jeton CSRF. Les jetons sont persistés en utilisant une session compatible ArrayAccess et sont générés à la demande.

Utilisation d'un jeton (token)

un jeton dans le cas présent, est une clé unique générée et stockée par le serveur pour chaque utilisateur effectuant une action sur notre site, le serveur doit vérifier la correspondance du jeton envoyé en recalculant cette valeur et en la comparant avec celle reçue par l'utilisateur lors de la soumission d'un formulaire.

👉🏽 ce token nous évitera le scenarios suivant :

<!-- site de l'attaquant -->
<form action="https://devs-cast.com/blog/1/delete" method="POST">
    <button>Vous avez gagné un voyage à paris</button>
</form>

Une personne 👨🏽‍💻 mal intentionnée peut créer un formulaire comme celui-ci sur son site, ce formulaire effectue une action de suppression d'article sur le mien (devs-cast), sans token et si la cible est authentifiée, elle supprimera un article, sans même sans rendre compte 🤷🏽‍♂️.

👉🏽 avec un token :

<!-- mon site -->
<form action="https://devs-cast.com/blog/1/delete" method="POST">
    <input type="hidden" name="delete_blog_1" value="afji9fj3dkdki3niadqer9>"/>
    <button>Supprimer</button>
</form>

Dans ce cas notre attaquant ne pourra pas deviner le token et si il essaye quand même le serveur ne prendra pas en compte sa requête et lui renverra une erreur 422 Unprocessable Entity ou 419 Page Expired (Utilisé par le Framework Laravel lorsqu'un jeton CSRF est manquant ou expiré. 💁🏽‍♂️)

Utiliser la méthode HTTP adaptée

🙅🏽‍♂️ Éviter d'utiliser des requêtes GET pour effectuer des actions : cette technique va naturellement éliminer des attaques simples basées sur les images, mais laissera passer les attaques fondées sur JavaScript, lesquelles sont capables très simplement de lancer des requêtes POST.

Voici un exemple d'actions et les méthodes HTTP adaptées :

  • affichage d'une ressource : GET
  • création d'une ressource : POST,
  • édition ou modification d'une ressource : PATCH ou PUT
  • suppression d'une ressource : DELETE

Vérifier l'origine de la requête

Une autre manière de se protéger serait de vérifier l'origine de la requête, et rejeter 🙅🏽‍♂️ toutes les requêtes ne provenant pas de votre site. voici une exemple :

<?php
function validRequest(): bool
{
    $myDomain = $_SERVER['SCRIPT_URI'];
    $requestsSource = $_SERVER['HTTP_REFERER'];

    return parse_url($myDomain, PHP_URL_HOST) === parse_url($requestsSource, PHP_URL_HOST);
}

Cette fonction renvoie 👍🏽true si la requête provient de votre site et 👎🏽false sinon. l'idée ici est de vérifier l'en-tête HTTP REFERER, mais notez que :

  • Elle peut être falsifiée (elle est envoyée par le navigateur, curl, client http, etc...)
  • Elle n'est pas toujours présente.

Donc cette vérification n'est pas une solution fiable mais reste valable 😉.

Conclusion

Pour conclure, la méthode la plus fiable reste celle de l'utilisation d'un jeton, d'ailleurs celle-ci est implémenter nativement sur les framework PHP modernes

Symfony :

$  composer require symfony/security-csrf

configuration dans la class formulaire :

<?php
// ...
use App\Entity\Task;
use Symfony\Component\OptionsResolver\OptionsResolver;

class TaskType extends AbstractType
{
    // ...

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Task::class,

            // activer/désactiver la protection du CSRF pour ce formulaire
            'csrf_protection' => true,

            // le nom du champ HTML caché qui stocke le jeton
            'csrf_field_name' => '_token',

            // une chaîne arbitraire utilisée pour générer la valeur du jeton
            // l'utilisation d'une chaîne de caractères différente pour chaque formulaire améliore sa sécurité
            'csrf_token_id'   => 'task_item',
        ]);
    }

    // ...
}

Dans le formulaire twig :

<form action="{{ url('admin_post_delete', { id: post.id }) }}" method="post">
    {# l'argument de csrf_token() est une chaîne arbitraire utilisée pour générer le toke #}
    <input type="hidden" name="token" value="{{ csrf_token('delete-item') }}"/>
    <button type="submit">Delete item</button>
</form>

Enfin la validation du token :

use Symfony\Component\HttpFoundation\Request;
// ...

public function delete(Request $request)
{
    $submittedToken = $request->request->get('token');

    // "delete-item" est la même valeur que celle utilisée dans le modèle pour générer le jeton
    if ($this->isCsrfTokenValid('delete-item', $submittedToken)) {
        // ... faire quelque chose, comme supprimer un objet
    }
}

👉🏽 Plus d'infos sur la documentation

Laravel

<form method="POST" action="/profile">
    @csrf
    ...
</form>

plutôt simple non ?

👉🏽 Plus d'infos sur la documentation