Retour au cours

backend / php-laravel

Sécurité : injection SQL, CSRF, mass assignment

Leçon 221 exercice

Explication

Ce que vous allez apprendre

  • Comprendre précisément pourquoi la concaténation SQL brute est dangereuse et comment l'éviter
  • Vérifier que la protection CSRF de Laravel est bien active sur les formulaires et les requêtes AJAX
  • Protéger un modèle contre le mass assignment en listant explicitement les champs modifiables
  • Reconnaître les situations où {!! !!} ouvre une faille XSS
  • Mettre en place une limitation de débit contre les attaques par force brute

Dans quel contexte ?

Un audit de sécurité externe pointe trois failles potentielles dans une application Laravel en production : un endpoint de tri accepte une valeur non validée directement dans une requête SQL brute, un modèle User accepte tous les champs sans liste blanche, et une page affiche des commentaires utilisateurs sans échappement. Ces trois problèmes, très différents en apparence, partagent une même cause profonde : faire confiance à une donnée venue de l'extérieur sans la valider ni l'échapper.

D'abord, la faille la plus connue mais toujours d'actualité : l'injection SQL

Concaténer directement une valeur utilisateur dans une requête SQL ("SELECT * FROM produits WHERE nom = '$nom'") permet à un attaquant d'injecter du SQL arbitraire en manipulant le contenu de $nom. Utiliser le query builder Eloquent ou des requêtes préparées avec bindings (DB::select('... WHERE nom = ?', [$nom])) élimine ce risque : la valeur est toujours traitée comme une donnée, jamais comme du code SQL exécutable.

Piège dangereux

whereRaw() et orderByRaw() sont tout aussi vulnérables qu'une requête SQL brute si une valeur utilisateur y est directement interpolée. La règle de sécurité : toute valeur destinée à du SQL brut doit être validée contre une liste blanche de valeurs acceptées (comme ['asc', 'desc']), jamais utilisée telle quelle.

Ensuite, une protection déjà activée par défaut qu'il ne faut pas casser

Laravel protège automatiquement chaque formulaire web contre le CSRF (Cross-Site Request Forgery) grâce à un token généré par @csrf. Une requête AJAX, elle, doit transmettre ce même token manuellement dans un en-tête X-CSRF-TOKEN, sans quoi Laravel la rejettera.

Type de requêteProtection CSRFComment
Formulaire HTML classiqueAutomatique@csrf dans le formulaire
Requête AJAX (même domaine)ManuelleEn-tête X-CSRF-TOKEN
API stateless (token Sanctum)Non nécessaireAuthentification par token, pas par cookie/session

Il reste une faille moins visible mais tout aussi sérieuse : le mass assignment

Sans $fillable explicite, User::create($request->all()) accepterait n'importe quel champ envoyé, y compris potentiellement un champ comme est_admin si un attaquant l'ajoute au payload de sa requête. Vue en leçon 10, cette protection redevient critique ici sous l'angle spécifiquement sécuritaire.

Bonne pratique

Pour des champs vraiment sensibles (rôle, solde, statut admin), ne compte jamais uniquement sur $fillable : assigne-les explicitement côté serveur ($user->est_admin = false;), jamais depuis une donnée qui pourrait provenir, même indirectement, d'une requête utilisateur.

Maintenant, une faille d'affichage tout aussi dangereuse

{!! $commentaire !!} désactive l'échappement automatique de Blade. Si $commentaire contient du contenu saisi par un utilisateur non fiable, un attaquant peut y injecter du JavaScript exécuté dans le navigateur de chaque visiteur qui consulte la page — une attaque XSS stockée, l'une des plus dommageables car elle touche tous les visiteurs, pas seulement l'attaquant.

Enfin, une protection contre les attaques automatisées

RateLimiter::for('login', fn ($request) => Limit::perMinute(5)->by($request->ip())) limite le nombre de tentatives de connexion par adresse IP, rendant une attaque par force brute (essayer des milliers de mots de passe) beaucoup plus lente et détectable.

Maintenant que les principales failles sont couvertes, il ne reste plus qu'à mettre cette application en production de façon fiable et performante : c'est l'objet de la dernière leçon de ce cours.

Commandes & code

Sécurité : injection SQL, CSRF, mass assignment

php
<?php
// INJECTION SQL : jamais de concaténation de valeurs utilisateur dans du SQL brut
// MAUVAIS -- vulnérable à l'injection
$nom = $request->input('nom');
$resultats = DB::select("SELECT * FROM produits WHERE nom = '$nom'"); // NE JAMAIS FAIRE ÇA

// BON -- requêtes préparées avec bindings, Eloquent/QueryBuilder le font automatiquement
$resultats = DB::select('SELECT * FROM produits WHERE nom = ?', [$nom]);
$resultats = Produit::where('nom', $nom)->get(); // toujours préféré : échappement automatique

// Même danger avec whereRaw / orderByRaw si on interpole sans binding
// MAUVAIS
Produit::orderByRaw("prix " . $request->input('direction')); // direction = "asc; DROP TABLE..."
// BON : valider contre une liste blanche avant d'utiliser une valeur dans du SQL brut
$direction = in_array($request->input('direction'), ['asc', 'desc']) ? $request->input('direction') : 'asc';
Produit::orderByRaw("prix $direction");
blade
{{-- CSRF : Laravel protège automatiquement les formulaires web via un token --}}
<form method="POST" action="/produits">
    @csrf {{-- génère un champ caché <input type="hidden" name="_token" ...> --}}
    <input type="text" name="nom">
</form>

{{-- Les requêtes AJAX doivent inclure le token dans un header --}}
<script>
fetch('/produits', {
    method: 'POST',
    headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
    body: JSON.stringify({ nom: 'Test' }),
});
</script>
php
<?php
// MASS ASSIGNMENT : sans $fillable/$guarded, create()/update() acceptent TOUT champ envoyé
// Un attaquant pourrait injecter 'est_admin' => true dans le payload

class User extends Authenticatable {
    // MAUVAIS : aucune protection, tout champ du payload est accepté
    // protected $guarded = [];

    // BON : liste blanche explicite des champs modifiables par l'utilisateur
    protected $fillable = ['nom', 'email', 'password'];
    // 'est_admin', 'solde', 'role' ne sont volontairement PAS fillable
}

// Pour les champs sensibles, toujours les assigner explicitement côté serveur, jamais depuis la requête
$user = User::create($request->only(['nom', 'email', 'password']));
$user->est_admin = false; // valeur forcée, jamais issue de l'input utilisateur

// Échappement des sorties : Blade {{ }} échappe déjà, mais attention à {!! !!}
// MAUVAIS si $commentaire vient d'un utilisateur non fiable
// {!! $commentaire !!}  -- exécute du HTML/JS arbitraire (XSS stocké)
// BON
// {{ $commentaire }}    -- toujours échappé

// Limitation de débit (rate limiting) contre le brute-force
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('login', function (Request $request) {
    return Limit::perMinute(5)->by($request->ip());
});
bash
# Audit de sécurité automatisé des dépendances
composer audit

Résumé

  • Toujours utiliser le query builder/Eloquent ou des requêtes préparées : jamais de concaténation SQL brute.
  • @csrf protège les formulaires web ; les routes API stateless via token n'en ont pas besoin (Sanctum).
  • $fillable est une liste blanche stricte : les champs sensibles (rôles, soldes) ne doivent jamais y figurer.
  • {!! !!} désactive l'échappement Blade : à réserver au contenu HTML explicitement sanitizé et de confiance.

Exercices pratiques

1 disponible
1

Mission : trois failles trouvees par un audit externe

Objectif : Corriger un endpoint de tri vulnerable, un modele expose au mass assignment, et justifier une regle de rate limiting.

Contexte

L'audit de securite mentionne dans la lecon pointe precisement l'endpoint GET /produits?direction=..., qui utilise Produit::orderByRaw("prix " . $request->input('direction')) sans aucune validation. Un testeur a envoye direction=asc; DROP TABLE produits;-- et bien que la requete ait echoue grace aux protections du driver, le champ reste dangereux tel quel. Le meme audit note que User::create($request->all()) est utilise dans le controleur d'inscription, avec un modele User dont $guarded = [].

Résoudre l’exercice →