Une petite protection brute force

publié le 14 sept. 2023

On a parfois besoin de protéger son application de visiteurs malveillants. Bien évidemment, l'authentification règle une sécurité simple efficace, mais pour certaines pages accessibles "sans mot de passe", quelle stratégie utiliser ?

La première stratégie consiste à créer des URL complexes, par exemple avec plusieurs chaînes de caractères longues (uuid) rendant le "guessing" des url statistiquement impossible. On peut également utiliser les url pré-signées, c'est-à-dire qui auront une sorte de token comme paramètre, token qui sera validé ou non par votre application.

Cette stratégie va calmer les ardeurs de petits malins qui voudraient explorer les routes "publiques" de votre application, pour autant cela n'empêchera pas des tentatives de "brute force" ou pour faire exploser votre serveur sous les requêtes.

Même si aucune solution n'est idéale, j'ai mis un petit middleware simple et qui permet d'écarter une partie du danger de ce type d'attaques:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;

class BruteForceProtection
{

    public function handle(Request $request, Closure $next)
    {
        // on détermine ce qu'est un visiteur avec une clé
		// une IP, une adresse MAC, à vous de choisir...
		$ipormac = $request->ip() ?? $request->mac();
        $key = 'ban:' . $ipormac;
        $banDuration = 10; // temps en minutes
        $maxFailedBeforeBan = 5;

		// on récupère les échecs comptabilisés
		// pour la clé donnée en cache
        $failedAttempts = Cache::store('redis')->get($key, 0); 
		

        $response = $next($request);
		// En cas de status 40x, on ajoute 1 échec au compteur d'échecs
        if ($response->status() >= 400 && $response->status() <= 404) {
            $failedAttempts++;
        }

        // Si le visiteur dépasse le seuil autorisé, il est banni
        if ($failedAttempts > $maxFailedBeforeBan) {
            Cache::store('redis')->put($key, $failedAttempts, now()->addMinutes($banDuration));
            return new Response('You have been banned...', 403);
        }

        Cache::store('redis')->put($key, $failedAttempts, now()->addMinutes(1));

        return $next($request);
    }

}

Le principe est le suivant :

=> quand un visiteur accède à une page donnée protégée par ce middleware, on va enregistrer l'adresse ip ou l'adresse mac de ce visiteur.

=> Si jamais la page revient en erreur, on comptabilise un échec pour ce visiteur.

=> Au bout de 5 échecs, le visiteur est banni pendant 10 minutes.

L'intérêt est surtout dissuasif : en espaçant les tentatives "possibles" à maximum 5 toutes les 10 minutes, cela devient juste trop long et trop coûteux pour le visiteur malveillant de faire son travail de sape, et il préférera probablement s'attaquer à une autre application...