backend / php-laravel
Sécurité : injection SQL, CSRF, mass assignment
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ête | Protection CSRF | Comment |
|---|---|---|
| Formulaire HTML classique | Automatique | @csrf dans le formulaire |
| Requête AJAX (même domaine) | Manuelle | En-tête X-CSRF-TOKEN |
| API stateless (token Sanctum) | Non nécessaire | Authentification 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
// 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");{{-- 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
// 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());
});# Audit de sécurité automatisé des dépendances
composer auditRésumé
- Toujours utiliser le query builder/Eloquent ou des requêtes préparées : jamais de concaténation SQL brute.
@csrfprotège les formulaires web ; les routes API stateless via token n'en ont pas besoin (Sanctum).$fillableest 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
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 = [].