Laravel 11

Cours Laravel 11 – la sécurité – on se protège

Lorsqu’on développe une application on prend plein de précautions, par exemple les utilisateurs doivent s’authentifier pour éviter des actions non autorisées. Dans le code on peut vérifier si la personne est authentifiée et quel est son degré d’habilitation. On a vu que Jetstream introduit une gestion complète des équipes avec des rôles et des permissions.

En dehors de l’authentification on doit gérer certaines situations comme par exemple savoir si un utilisateur authentifié a le droit de modifier une ressource particulière. Laravel nous offre un système d’autorisations bien pratique.

L’authentification

Les middlewares

On a déjà vu l’authentification en détail dans un précédent chapitre. On peut aussi gérer les utilisateurs authentifiés selon un rôle ou leur degré d’habilitation sans nécessairement utiliser Jetstream. On peut filtrer les accès selon un statut avec des middlewares. Par exemple on peut créer un middleware Admin :

php artisan make:middleware IsAdmin

Le dossier Middleware a été créé ainsi que notre middleware avec ce code par défaut :

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class IsAdmin
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }
}

Il ne nous reste lus qu’à ajouter notre logique :

public function handle(Request $request, Closure $next): Response
{
    $user = $request->user();
    if ($user && $user->role === 'admin') {
        return $next($request);
    }
    return redirect()->route('home');
}

Dans la méthode handle on récupère l’utilisateur en cours ($request->user()) et on teste s’il s’agit d’un administrateur en imaginant que dans la table users on a créé une colonne role ($user->role === ‘admin’).

Il nous faut maintenant enregistrer ce middleware pour informer Laravel qu’on l’a créé si on le veut actif sur toutes les routes, sinon pas besoin de l’enregistrer. Ca se passe dans le fichier boostrap/app.php :

use app\Http\Middleware\IsAdmin;

return Application::configure(basePath: dirname(__DIR__))
    ...
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->append(IsAdmin::class);
    })

On peut ainsi filtrer les routes réservées aux administrateurs :

use App\Http\Middleware\IsAdmin;

Route::middleware(IsAdmin::class)->group(function () {
    ...
}

Pour plus de simplicité et d’élégance on peut créer un alias (toujours dans boostrap/app.php) :

->withMiddleware(function (Middleware $middleware) {
    ...
    $middleware->alias([
        'admin' => IsAdmin::class
    ]);
})

Et c’est plus simple à l’utilisation :

Route::middleware('admin')->group(function () {

Le throttle

Jusqu’à la version 7, Laravel intégrait automatiquement dans l’authentification une sécurisation de la connexion contre les attaques « brute force ». On disposait d’un trait  Illuminate\Foundation\Auth\AuthenticatesUsers qu’il suffisait d’inclure dans le contrôleur de connexion. A compter de la version 8 il a fallu utiliser Breeze ou Jetstream pour en bénéficier. Au niveau du fonctionnement, l’utilisateur est bloqué pendant une minute au bout d’un certain nombre d’essais infructueux.

Rate limiter

On dispose d’un limiteur d’accès qu’on peut appliquer à une route ou un groupe de routes. On utilise le provider AppServiceProvider. Il y a déjà ce code par défaut :

...
use Illuminate\Support\Facades\RateLimiter;
 
protected function boot(): void
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}

Ici on a un limiteur qui va fixer le maximum à 60 accès par minutes pour les API. Au-delà on obtient une erreur 429 ou une réponse personnalisée. Il est possible d’affiner selon l’utilisateur ou le type, je vous renvoie à la documentation pour ces précisions.

Pour l’application du limiteur on utilise un middleware :

Route::middleware(['throttle:uploads'])->group(function () {
    Route::post('/articles', function () {
        //
    });

    ...
});

Quelques éléments à prendre en considération

Les injections SQL

L’injection SQL (SQLi) est un problème de sécurité fréquent dans les applications web. Il se produit quand une application accepte des données de l’utilisateur sans les vérifier correctement, ce qui permet à un attaquant malintentionné d’influencer les requêtes SQL envoyées à la base de données.
Imaginez une application web qui demande un nom. Si l’application n’est pas configurée correctement, un attaquant pourrait entrer un nom spécial qui contient du code SQL au lieu d’un nom normal. Ce code SQL peut être exécuté par l’application, ce qui peut causer des problèmes.
Par exemple, un attaquant pourrait faire en sorte de récupérer des informations sensibles, comme les mots de passe d’autres utilisateurs, ou même modifier ou supprimer des données dans la base de données. C’est pourquoi il est important pour les développeurs d’applications web de vérifier toujours les données fournies par les utilisateurs et de les nettoyer correctement avant de les utiliser dans des requêtes SQL.
Prenons une analogie. Vous demandez à un ami d’apporter un livre. Si on ne précises pas clairement quel livre on veut, cet ami pourrait apporter n’importe quel livre. De même, si une application web n’est pas configurée correctement, elle pourrait exécuter n’importe quel code SQL, ce qui peut être dangereux.

Si vous utilisez Eloquent vous êtes automatiquement immunisé contre ces attaques.

Par contre si vous faites des requêtes avec par exemple DB::raw() ou whereRaw, autrement dit si vous contournez Eloquent, alors vous devez vous-même vous prémunir contre les attaques SQL.

CSRF

Je vous ai déjà parlé de la protection CSRF dans ce cours, elle est automatiquement mise en place par Laravel.

XSS (Cross-Site Scripting)

Le Cross-Site Scripting (XSS) est une autre vulnérabilité de sécurité web courante, qui se produit lorsqu’une application web permet à un attaquant d’injecter du code malveillant dans les pages web affichées aux utilisateurs. Ce code malveillant est exécuté dans le navigateur de l’utilisateur, ce qui peut avoir des conséquences dangereuses.
L’injection de code malveillant peut se produire lorsque les données d’entrée de l’utilisateur, comme les commentaires ou les messages de forum, ne sont pas correctement filtrées ou échappées par l’application web. Cela signifie que le code malveillant est traité comme du code HTML ou JavaScript légitime et est exécuté par le navigateur.
Les attaques XSS peuvent être utilisées pour voler des informations sensibles, comme les cookies de session ou les informations d’identification des utilisateurs. Dans certains cas, elles peuvent même permettre à un attaquant de prendre le contrôle complet du compte d’un utilisateur.
Pour se protéger contre les attaques XSS, les développeurs doivent veiller à :
  1. Filtrer et valider correctement toutes les données d’entrée.
  2. Échapper les caractères spéciaux qui pourraient être interprétés comme du code HTML ou JavaScript.
  3. Utiliser des headers de sécurité, tels que Content-Security-Policy, pour restreindre les sources de scripts et autres ressources externes.
En résumé, le XSS est une vulnérabilité de sécurité web qui peut avoir des conséquences sérieuses si elle n’est pas correctement gérée. Il est important pour les développeurs de prendre des mesures de sécurité adéquates pour protéger leurs applications web contre ce type d’attaque.

Laravel ne fait pas de nettoyage des entrées en dehors de la validation. Par contre vous avez avec Blade la syntaxe sécuritaire {{ .. }} qui « échappe » les données à l’affichage.

Faites très attention avec la syntaxe {!! … !!} ! En gros vous devez l’éviter avec des données issues de l’extérieur de l’application.

L’assignation de masse

Je vous ai déjà parlé de l’assignation de masse. Vous devez choisir avec soin les colonnes des tables qui sont sans danger dans une mise à jour de masse, celles que vous mettez dans la propriété $fillable (ou $guarded avec la logique inverse) d’un modèle.

Les requêtes de formulaire

Un autre lieu où on peut sécuriser l’application est au niveau des requêtes de formulaires. On a vu qu’il y a une méthode authorize.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

abstract class Request extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    ...
}

Vous pouvez ici vérifier par exemple si un utilisateur a le droit d’utiliser effectivement le formulaire et renvoyer false si ce n’est pas le cas.

Généralités

Il y a des choses qui vont sans dire mais beaucoup mieux en les disant. Laravel ne peut pas être en toute sécurité sur un serveur qui ne l’est pas, donc le choix de l’hébergement est un facteur important à prendre en compte et la présence d’un bon Firewall est indispensable. De plus le SSH devient de nos jours incontournable.

Il faut aussi régulièrement mettre à jour Laravel et les packages associés. Il y a régulièrement des correctifs de sécurité et si on ne les installe pas on se retrouve exposé.

Faites des sauvegarde de votre site et surtout de la base de données ! On n’est jamais à l’abri d’un accident…

Les autorisations

En plus de ces possibilités Laravel nous offre un système complet d’autorisations. Pour voir de quoi il s’agit on va prendre un exemple simple. On va créer une ressource pour les utilisateurs à partir d’une installation de base de Laravel équipé de l’authentification, par exemple avec Breeze comme on l’a déjà vu dans ce cours :

php artisan make:controller UserController --resource

Changez ainsi le code de ces deux fonctions :

public function edit($id)
{
    return 'Formulaire pour modifier';
}

public function update(Request $request, $id)
{
    return 'Ok on a modifié !';
}

Et les routes correspondantes qu’on protège avec le middleware auth :

use App\Http\Controllers\UserController;

Route::resource('users', UserController::class)->middleware('auth');

Maintenant on est sûrs que seules les personnes authentifiées peuvent accéder à la ressource mais… si quelqu’un veux faire une modification sur un utilisateur, donc passer par les routes users.edit et users.update il serait souhaitable de ne laisser l’accès à un utilisateur que pour ses propres informations. Autrement dit si on a l’utilisateur avec l’id 6 il ne doit avoir accès qu’aux urls :

  • users/6
  • users/6/edit

Comment réaliser celà ?

On dispose d’une commande pour créer une autorisation :

php artisan make:policy UserPolicy --model=User

Comme on a mentionné le modèle on a déjà toutes les méthodes prêtes :

<?php

namespace App\Policies;

use App\Models\User;
use Illuminate\Auth\Access\Response;

class UserPolicy
{
    /**
     * Determine whether the user can view any models.
     */
    public function viewAny(User $user): bool
    {
        //
    }

    /**
     * Determine whether the user can view the model.
     */
    public function view(User $user, User $model): bool
    {
        //
    }

    /**
     * Determine whether the user can create models.
     */
    public function create(User $user): bool
    {
        //
    }

    /**
     * Determine whether the user can update the model.
     */
    public function update(User $user, User $model): bool
    {
        //
    }

    /**
     * Determine whether the user can delete the model.
     */
    public function delete(User $user, User $model): bool
    {
        //
    }

    /**
     * Determine whether the user can restore the model.
     */
    public function restore(User $user, User $model): bool
    {
        //
    }

    /**
     * Determine whether the user can permanently delete the model.
     */
    public function forceDelete(User $user, User $model): bool
    {
        //
    }
}

Comme Laravel est conciliant, si on respecte les appelations, ce qui est le cas ici parce que j’ai respecté le nom du modèle, la déclaration est automatique. On va donc protéger la méthode update :

public function update(User $user, User $model): bool
{
    return auth()->id() === $model->id;
}

Il ne reste plus qu’à utiliser l’autorisation dans le contrôleur :

use App\User;

...


public function edit(User $user)
{
    $this->authorize('update', $user);

    return 'Formulaire pour modifier';
}

public function update(Request $request, User $user)
{
    $this->authorize('update', $user);
    
    return 'Ok on a modifié !';
}

Et maintenant si c’est le bon utilisateur il a l’accès sinon il tombe sur une erreur 403.

Plutôt que d’intervenir dans chaque méthode de la ressource on peut aussi faire agir l’autorisation globalement sur la ressource avec ce code dans le constructeur :

public function __construct()
{
    $this->authorizeResource(User::class, 'user');
}

Vous pouvez trouver la documentation complète ici.

En résumé

  • L’authentification permet de sécuriser une application et d’adapter l’affichage selon le degré d’habilitation de l’utilisateur.
  • Eloquent immunise contre les injections SQL.
  • La protection CSRF est automatiquement mise en place par Laravel.
  • Il faut être très prudent avec la syntaxe {!! … !!} de Blade.
  • On peut sécuriser les requêtes de formulaire avec leur méthode authorize.
  • Laravel comporte un système complet et simple de gestion des autorisations (policies).

 

Print Friendly, PDF & Email

2 commentaires

  • GrCOTE7

    Bonjour,

    J’ai observé que si dans boostrap/app.php, on déclare le middleware, cela entraine une erreur de redirections infinies (et qu’à contrario, sans cette déclaration infinie, le middleware fonctionne néanmoins parfaitement).

    À noter que boostrap/app.php sera par contre vraiment utile pour y déclarer un alias des middlewares.

    Est-ce le cas pour tous, ou peut-être ai-je autre chose qui expliquerait l’inutilité de la déclaration du middleware ?

    • bestmomo

      Salut,

      Je viens de tester, effectivement ça fonctionne même sans la déclaration. Par contre, je ne rencontre pas ton problème lorsque je déclare le middleware. Il faut quand même préciser qu’on ne doit enregistrer le middleware que si on le veut actif sur toutes les routes, dans le cas contraire, il n’est pas besoin de l’enregistrer.

Laisser un commentaire