Tu reprends une app Laravel en urgence. Elle “tourne”, les pages s’affichent, les tests (s’il y en a) passent. Et pourtant, je parie un café que tu vas trouver des trous. Pas des trucs ésotériques, pas des failles hollywoodiennes. Des oublis bêtes, répétitifs, qui arrivent quand une codebase a vécu, a changé d’équipe, et que la sécurité a été repoussée « à plus tard »… jusqu’au jour où tu deviens le ou la personne qui doit déployer.
Ce que je te donne ici, c’est mon playbook de reprise : un triage rapide, puis 15 oublis sécurité que je retombe (presque) à chaque fois sur du Laravel “hérité”. L’idée n’est pas de tout refaire. L’idée, c’est de mettre des garde-fous maintenant, et de transformer le reste en backlog assumé.
Triage en 30 minutes : savoir si tu es assis sur une bombe
Quand je n’ai pas le luxe du temps, je commence toujours par trois endroits : le fichier .env (ou les variables en prod), la config (surtout config/session.php, config/auth.php, config/cors.php), et la surface d’attaque réelle via php artisan route:list.
Dans ce triage, tu cherches des signaux. Oubli #1 : APP_DEBUG=true ou un handler d’erreur trop bavard en prod. Même quand “personne ne voit l’erreur”, un stacktrace exposé, c’est un plan de ton app offert au premier curieux. Oubli #2 : des outils de prod/dev (Telescope, Horizon, Debugbar) accessibles sans restriction solide. C’est pratique, et c’est aussi une autoroute si c’est exposé sur Internet avec un pauvre check IP approximatif ou, pire, rien du tout.
Et tu peux déjà détecter le niveau de maturité global avec deux détails. Est-ce que l’app force HTTPS et des cookies propres ? Est-ce que l’auth est entourée de rate limiting ? Si la réponse est “bof”, je sais déjà que les autres oublis vont s’empiler.
Sponsorisé par Le Scribouillard
Besoin de contenu optimisé SEO ?
Utilisez la meilleure plateforme française de création de contenu assistée par IA ! Et générez des articles pour moins de 1€ !
Auth, sessions, cookies : les failles “pas glamour” qui font très mal
Laravel fait beaucoup de choses bien par défaut, mais une app legacy a souvent des réglages “hérités” ou bricolés. Oubli #3 : des cookies de session pas assez stricts. En prod, je veux voir SESSION_SECURE_COOKIE=true derrière HTTPS, un same_site cohérent (souvent lax, parfois strict selon le produit), et je veux éviter les comportements ambigus quand l’app est derrière un proxy / un load balancer. Sur les vieilles stacks, le combo proxy mal déclaré + génération d’URL en HTTP peut finir en cookies non sécurisés “par accident”.
Oubli #4 : un driver de session qui ne colle pas au déploiement réel. Le classique : SESSION_DRIVER=file sur plusieurs instances, puis des déconnexions bizarres, et des contournements de sécurité ajoutés “pour stabiliser” (spoiler : c’est rarement ça qu’il fallait faire). Sur un environnement multi-serveurs, Redis ou la base de données rendent la session plus prédictible et moins fragile.
Je regarde aussi si des endpoints sensibles existent en “anonyme” parce que “c’est une API” ou “c’est du mobile”. On peut faire de l’API propre, mais pas en laissant le monde entier taper des routes qui mutent des données.
Rate limiting : le trou que tout le monde connaît… et que personne ne ferme
Si je ne devais corriger qu’un seul truc sur une reprise, ce serait souvent celui-là. Oubli #5 : pas de throttling sur /login (ou un throttling trop gentil). Et oubli #6 : pas de throttling sur les endpoints “oubliés” qui valent de l’or pour un attaquant, typiquement /forgot-password, /password/email, /two-factor, /otp, ou même des endpoints de recherche utilisateur qui permettent de faire de l’énumération.
Je ne parle pas de « sécurité parfaite », je parle de stopper la casse la plus évidente : brute force, credential stuffing, spam de reset password. Le gain est immédiat, et tu peux le faire sans réécrire l’app.
// Exemple simple : forcer un throttle explicite sur des routes sensibles
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
RateLimiter::for('login', function (Request $request) {
$key = strtolower((string) $request->input('email')).'|'.$request->ip();
return Limit::perMinute(5)->by($key);
});
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:login');
Route::post('/forgot-password', [PasswordResetController::class, 'sendLink'])
->middleware('throttle:5,1');Oui, le throttle “à la route” est un peu brut. Mais en reprise d’urgence, c’est exactement ce que je veux : un filet de sécurité visible, auditable, et difficile à oublier.
Reset password : tokens, expiration, et petites erreurs qui se payent cash
Le reset password, c’est une zone où je vois des réglages par défaut gardés “par flemme”, ou des customisations dangereuses faites pour “améliorer l’UX”. Oubli #7 : une durée de validité des tokens trop longue, ou un flux où le token peut être réutilisé sans invalidation nette. Ce n’est pas toujours une vulnérabilité évidente dans le code, c’est souvent une combinaison : token valable longtemps + mails consultables sur un poste partagé + pas de rate limiting + logs qui contiennent des URLs sensibles. Et tu te retrouves avec un incident impossible à expliquer proprement.
Sur Laravel, je vérifie concrètement la config des password brokers, le stockage (table password_reset_tokens selon versions), et le throttle. Et je regarde aussi les emails envoyés : si des liens de reset finissent dans des logs ou des outils de tracking, tu as un problème de process autant que de code.
Autorisation (policies, gates) : le faux sentiment de sécurité du middleware auth
Sur une app legacy, le pattern « j’ai mis auth donc c’est bon » est partout. Sauf que auth dit juste “quelqu’un est connecté”, pas “il a le droit”. Oubli #8 : pas de policies, ou des policies déclarées mais jamais appelées. Oubli #9 : des contrôleurs qui ont des méthodes admin cachées au fond, accessibles à n’importe quel user loggé, parce que personne n’a mis authorize() ou can: sur la route.
Le piège vicieux, c’est la route “temporaire” qui devient permanente. Un endpoint pour “débloquer un compte” ou “rejouer une facture” créé en incident, puis oublié. C’est pour ça que je repasse systématiquement sur route:list en filtrant sur des mots-clés qui sentent l’admin, le support, le backoffice, ou le debug. Et quand je trouve un truc douteux, je préfère être sec : je ferme, je mets une policy, et je mets le débat produit après.
Mass assignment : le classique Laravel qui revient dès que ça va vite
Celui-là, je le vois littéralement en reprise. Oubli #10 : des modèles avec protected $guarded = []; “pour aller plus vite”. Et oubli #11 : des contrôleurs qui font du Model::create($request->all()) ou $model->update($request->all()). Si tu combines les deux, tu as un boulevard pour modifier des champs que personne ne voulait exposer (rôle, statut, prix, flags internes…).
Mon approche en urgence est pragmatique : je verrouille d’abord les modèles critiques (users, billing, permissions, tout ce qui a un impact financier ou d’accès), puis je remplace petit à petit les all() par du validated(). Ce n’est pas “beau”, mais ça coupe la classe d’attaque la plus rentable.
Validation : FormRequest, validation serveur, et le mythe du “le front bloque déjà”
Je ne compte plus le nombre de fois où je lis « c’est validé côté front » dans une issue. Ça ne compte pas. En reprise, je cherche surtout où est la validation et si elle est fiable. Oubli #12 : validation dispersée dans les contrôleurs, incomplète, non réutilisée, avec des conditions implicites. Le jour où une route est appelée autrement (script, API, import), tu perds la protection.
Ce que je préfère en Laravel, c’est déplacer la pression dans des FormRequest : tu centralises les règles, tu peux y mettre de l’autorisation, et tu forces l’usage de $request->validated(). Ce n’est pas un débat de “propreté”, c’est un moyen de réduire les entrées non maîtrisées.
Et il y a un endroit où cette validation devient non négociable : les uploads.
Uploads : la faille la plus sous-estimée sur les apps qui “acceptent des fichiers”
Oubli #13 : accepter un fichier parce que son extension “a l’air OK”. Ou vérifier un MIME type envoyé par le client. Ou autoriser des SVG “parce que c’est une image”. Les uploads, c’est un terrain miné : XSS via SVG, fichiers polyglottes, bombes zip, stockage public, noms de fichiers prévisibles, et parfois exécution involontaire selon la conf serveur.
Oubli #14 : stocker directement dans public/ avec le nom original. Ce n’est pas toujours catastrophique… jusqu’au jour où un utilisateur uploade un fichier avec un nom qui casse ton UI, où un crawler indexe des documents internes, ou où tu te retrouves à servir des fichiers non prévus sans contrôle d’accès. Sur une reprise, je préfère stocker via Storage en disque non public, générer des noms random, et servir via une route signée ou un contrôleur qui vérifie l’autorisation.
Si tu n’as pas le temps de faire une refonte, tu peux déjà gagner beaucoup avec une validation serveur stricte (taille, types, dimensions d’images si pertinent), et en refusant certains formats à risque. Ce n’est pas “parano”, c’est juste réaliste.
CORS, API tokens et environnements : les réglages qui ouvrent l’app au monde entier
Quand une app a un front séparé, une app mobile, un peu d’API, les configs finissent souvent en « on ouvre et on verra après ». Mauvaise idée. Oubli #15 : CORS trop permissif, surtout quand il est combiné à une auth stateful (cookies) type Sanctum. Un allowed_origins en * ou un pattern trop large, et tu te rajoutes des scénarios de CSRF “modernes” qui n’étaient pas dans la tête de l’équipe au moment où ça a été mis.
Je regarde aussi l’hygiène des secrets. Pas en mode police, en mode survie. Est-ce que des tokens traînent dans des commits ? Est-ce que l’app log des Authorization headers ? Est-ce que le repo contient un vieux .env « d’exemple » qui est en fait un .env réel ? Ça arrive plus qu’on ne veut l’admettre.
Et côté dépendances, je veux une vérité simple : est-ce qu’on a au moins un garde-fou en CI ? Pas pour avoir un score, pour éviter de déployer avec une CVE connue et triviale.
Ce que je patch tout de suite vs ce que je mets en backlog (sans me mentir)
En reprise d’urgence, le piège, c’est de partir dans une “refacto sécurité” qui dure trois semaines. Mon pattern est plus agressif : je coupe d’abord les accès évidents (endpoints admin, outils exposés), je mets du rate limiting, je verrouille mass assignment, je durcis les uploads. Ça, c’est du patch ciblé qui réduit le risque rapidement.
Ensuite seulement, je transforme le reste en backlog propre : policies systématiques, migration progressive vers des FormRequest, tests de non-régression sur routes sensibles, audit des logs (PII, tokens), nettoyage des configs d’environnements. Et surtout, j’écris noir sur blanc ce qu’on a accepté comme risque temporaire. Une reprise, c’est aussi de la gestion d’attentes.
Un mini gabarit à copier : ma checklist “reprise Laravel” (version brute)
# Reprise Laravel — checklist sécurité (brute)
## Triage
- APP_DEBUG off, erreurs non verbeuses en prod
- Outils internes (Telescope/Horizon/Debugbar) non exposés ou verrouillés
- route:list : repérer endpoints admin/support/debug oubliés
## Auth / sessions
- Cookies secure + SameSite cohérent
- Session driver adapté au déploiement (multi-instances = pas file)
## Rate limiting
- Throttle login
- Throttle forgot/reset password, OTP/2FA, endpoints d’énumération
## Reset password
- Expiration raisonnable, pas de réutilisation, throttle
## Autorisation
- Policies/Gates réellement appelées sur les actions sensibles
- Pas de “auth donc ok” sur de l’admin
## Données
- Mass assignment verrouillé (pas de $guarded = [])
- create/update sur validated(), pas sur all()
## Validation
- Validation centralisée (FormRequest), règles cohérentes
## Uploads
- Validation serveur (type/size), formats à risque traités
- Stockage non-public si contenu privé, noms random
## CI/CD
- composer audit (au moins) + scan de secrets
- Smoke test : routes sensibles inaccessibles sans droitCe gabarit n’est pas une religion. C’est juste une base pour ne pas oublier l’évident quand tu es sous pression.
Conclusion : sécuriser une app legacy, ce n’est pas “tout blinder”, c’est choisir les bons combats
Sur une reprise, ton vrai ennemi n’est pas Laravel. C’est le mélange de dettes invisibles, de configs approximatives et de routes “temporairement” ouvertes. Si tu attaques ça avec des patchs courts et des garde-fous (middleware, FormRequest, policies, stockage), tu gagnes vite en sécurité sans mettre le produit à l’arrêt.
Et après le déploiement, tu fais ce que beaucoup ne font pas : tu gardes la checklist vivante. À chaque incident, à chaque nouveau module, tu ajoutes une ligne. Une app legacy, ça ne devient pas propre d’un coup. Mais ça peut devenir nettement moins dangereuse en une journée.