La plupart des développeurs pensent à la sécurité comme à une étape — un audit à faire après avoir codé une feature. La recherche en sécurité t'apprend que c'est une façon de penser.
Le changement de perspective
Quand tu développes, tu penses à ce que le code devrait faire. Quand tu cherches des vulnérabilités, tu penses à ce que le code pourrait faire si quelqu'un l'utilise différemment de ce que tu avais prévu.
Ces deux postures changent radicalement la façon dont tu modélises les problèmes.
Les erreurs les plus fréquentes que j'observe
1. Faire confiance à l'entrée utilisateur trop tôt (IDOR)
La faille IDOR (Insecure Direct Object Reference) consiste à manipuler un identifiant de ressource dans la requête pour accéder ou modifier les données d'un autre utilisateur.
// ❌ DANGEREUX — l'ID vient de la requête sans vérification de propriété
public function destroy(Request $request): RedirectResponse
{
Document::findOrFail($request->id)->delete();
return back();
}
// SÉCURISÉ — Laravel Policy valide l'ownership
public function destroy(Document $document): RedirectResponse
{
$this->authorize('delete', $document);
$document->delete();
return back();
}La première version fonctionne parfaitement en développement. En production, n'importe quel utilisateur authentifié peut supprimer le document d'un autre en changeant simplement la valeur de l'identifiant.
2. Les mass assignment (affectation de masse)
Le mass assignment survient quand un utilisateur soumet des paramètres inattendus (ex: is_admin = true) et que l'application les persiste aveuglément en base de données.
// ❌ RISQUÉ — si le modèle n'a pas de $fillable ou $guarded restrictif
User::create($request->all());
// SÉCURISÉ — définition explicite des champs autorisés
User::create($request->only(['name', 'email', 'password']));3. Les conditions de concurrence (Race Conditions)
Ce type de bug survient lorsque deux requêtes concurrentes s'exécutent simultanément pour manipuler une ressource critique (ex: un solde de compte ou un stock), contournant les vérifications d'état.
// ❌ DANGEREUX — les deux requêtes simultanées peuvent passer le check avant le décrément
if ($wallet->balance >= $amount) {
$wallet->decrement('balance', $amount);
}
// SÉCURISÉ — verrouillage pessimiste lockForUpdate dans une transaction SQL
DB::transaction(function () use ($wallet, $amount) {
$wallet->lockForUpdate()->find($wallet->id);
if ($wallet->balance < $amount) {
throw new InsufficientFundsException();
}
$wallet->decrement('balance', $amount);
});Attention : Ce type de bug est invisible dans tes tests unitaires standards et difficile à reproduire manuellement. Il nécessite des transactions SQL rigoureuses et du verrouillage pessimiste (lockForUpdate()).
Ce que ça change dans mon approche quotidienne
Modélisation des menaces
Pour chaque endpoint créé, je me pose trois questions : Qui peut l'appeler ? Que se passe-t-il si deux actions surviennent en parallèle ? Que peut envoyer un attaquant ?
Autorisations centralisées (Policies)
Plutôt que d'éparpiller des vérifications if ($user->isAdmin()) dans les contrôleurs, je centralise l'autorisation dans des classes de Policies Laravel dédiées.
Validation des transitions d'état
Un objet ne change pas d'état (ex: de draft à published) de manière arbitraire. Les transitions d'état sont contrôlées par un automate ou un pattern State.
Zero-Trust interne
Je ne fais pas confiance absolue aux données qui viennent de la base de données sans validation préalable si elles influencent une décision critique (rôles, soldes).
La sécurité comme design, pas comme couche
La conclusion que j'ai tirée de ces expériences : la sécurité ne s'ajoute pas après. Elle s'intègre dans les décisions d'architecture dès le départ.
Un code bien structuré, avec des couches de responsabilités claires (Services, Policies, Transactions) et des politiques d'autorisation centralisées, est naturellement beaucoup plus difficile à compromettre.