Laravel security in productie: een praktische checklist met risico, check en fix. Van APP_DEBUG en IDOR tot composer audit, rate limiting en dashboards.
Eén verkeerde regel in je .env is genoeg. APP_DEBUG=true op productie laat bij een exception je configuratie, paden en stack trace zien aan iedereen die de fout kan uitlokken. Geen exploit nodig.
Laravel is standaard veilig genoeg om mee te beginnen. Je krijgt CSRF-bescherming, prepared statements en Blade-escaping zonder iets te doen. De gaten ontstaan daarna: in je configuratie, in je autorisatie en in de plekken waar je om het framework heen bouwt.
Dit is een checklist voor Laravel security in productie. Gericht op developers en CTO's die een bestaande applicatie willen hardenen. Werk je met Filament? Dan staat het admin-panel in de Filament beveiligingschecklist. Hier gaat het om de applicatie eronder.
Overzicht: risico, check, fix
| Risico | Check | Fix |
|---|---|---|
| Debug-output zichtbaar | APP_DEBUG op productie | APP_DEBUG=false, APP_ENV=production |
Gelekte .env | Is /.env bereikbaar via de webserver? | Document root op public/, deny-regel in nginx |
| Gestolen sessies of cookies | APP_KEY, cookie-flags | Key roteren na lek, SESSION_SECURE_COOKIE=true |
| IDOR | Kan gebruiker A /orders/123 van B openen? | Policies, queries scopen op eigenaar |
| Mass assignment | $guarded = [] in models | Expliciete $fillable, validated() gebruiken |
| SQL injection | DB::raw, whereRaw met input | Bindings gebruiken |
| XSS | {!! !!} in Blade | {{ }} of sanitizen met allowlist |
| Brute force | Login en API zonder limiet | throttle middleware, RateLimiter |
| Kwaadaardige uploads | Mime, extensie, opslaglocatie | Validatie, private disk, geen uitvoerbare bestanden |
| Kwetsbare dependencies | Bekende advisories | composer audit in CI |
| Open dashboards | Horizon, Telescope, Pulse | Gate op toegestane gebruikers, Telescope uit |
| Lekkende logs | Wachtwoorden, tokens, PII in logs | dontFlash, redacten, retentie |
De rest van dit artikel loopt de tabel door.
1. Configuratie en omgeving
APP_DEBUG en APP_ENV
De Laravel deployment-documentatie zegt het letterlijk: op productie moet APP_DEBUG false zijn. Zet daarbij APP_ENV=production.
APP_ENV=production
APP_DEBUG=false
Check ook je foutpagina's. Een 500 mag niets anders tonen dan een generieke melding.
.env mag nooit bereikbaar zijn
Je document root is public/, niet de projectroot. Test het zelf: curl -I https://jouwdomein.nl/.env moet een 404 geven. Op een Forge-server staat dit goed ingesteld, maar controleer het na elke handmatige nginx-wijziging.
Commit .env nooit. Zet ook .env.backup en vergelijkbare bestanden niet in public/.
APP_KEY
De APP_KEY versleutelt cookies en sessiedata. Lekt de key, dan kan een aanvaller versleutelde payloads vervalsen. Roteer hem bij een vermoeden van lekkage. Laravel ondersteunt APP_PREVIOUS_KEYS, zodat bestaande sessies en versleutelde data niet meteen ongeldig worden. Controleer de details in de docs van jouw versie.
Config cachen
Draai php artisan optimize (of config:cache) bij elke deploy. Daarna leest Laravel .env niet meer. Gebruik env() dus alleen in config-bestanden, anders krijg je null terug. Dat is een bekende bron van "hij werkt lokaal, niet op productie".
Trusted proxies en hosts
Staat je app achter een load balancer of Cloudflare? Dan moet Laravel weten welke proxies je vertrouwt. Anders klopt request()->ip() niet en worden rate limits en logs onbetrouwbaar. Sinds Laravel 11 stel je dit in bootstrap/app.php in:
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: ['10.0.0.0/8']);
$middleware->trustHosts(at: ['^(.+\.)?jouwdomein\.nl#x27;]);
})
Vertrouw niet blind '*' als je server ook direct bereikbaar is. Dan kan een client zelf headers als X-Forwarded-For meesturen. Gebruik trustHosts tegen host header poisoning, bijvoorbeeld bij wachtwoord-reset mails met een verkeerde link.
2. Autorisatie
Authenticatie bewijst wie je bent. Autorisatie bepaalt wat je mag. De meeste echte datalekken zitten hier, niet in een exotische exploit.
IDOR
Insecure Direct Object Reference: gebruiker A past /orders/123 aan naar /orders/124 en ziet de bestelling van B. Route model binding lost dit niet op. Het haalt het record alleen op.
Twee verdedigingslinies:
// 1. Policy
public function view(User $user, Order $order): bool
{
return $order->user_id === $user->id;
}
// 2. In de controller
$this->authorize('view', $order);
// of scope de query direct
$order = $request->user()->orders()->findOrFail($id);
Gebruik de tweede variant voor alles wat op een eigenaar hangt. Een vergeten authorize() kan dan geen data meer lekken.
Test dit. Schrijf per resource een test die als gebruiker A het object van gebruiker B opvraagt en een 403 of 404 verwacht.
Mass assignment
$guarded = [] samen met Model::create($request->all()) is een open deur. Een gebruiker stuurt is_admin=1 mee en je model slikt het.
- Definieer
$fillableexpliciet. - Gebruik
$request->validated()in plaats van$request->all(). - Zet rol- en rechtenvelden nooit in
$fillable.
3. Input en output
Validatie
Valideer alles aan de rand, in een Form Request. Gebruik allowlists (in:, exists:, enums) in plaats van "alles behalve". Valideer ook JSON-bodies van je API's, niet alleen formulieren.
SQL injection
Eloquent en de query builder gebruiken prepared statements. Het risico zit in raw queries met gebruikersinput.
// Fout
User::whereRaw("email = '$email'")->first();
// Goed
User::whereRaw('email = ?', [$email])->first();
Let ook op orderBy($request->sort). Kolomnamen kunnen niet gebonden worden. Valideer ze tegen een vaste lijst.
Zoek in je codebase naar DB::raw, whereRaw, selectRaw, orderByRaw en DB::statement. Controleer elke hit op input.
XSS in Blade
{{ $var }} escaped output. {!! $var !!} doet dat niet. Zoek op {!! en beoordeel elke plek. Moet je HTML tonen (bijvoorbeeld uit een rich text editor), sanitize dan server-side met een allowlist-library. Escapen bij weergave is niet genoeg als je de HTML zelf vertrouwt.
CSRF
Het web-middleware-pakket regelt CSRF-tokens. Controleer drie dingen:
- Staat er geen route in de CSRF-uitzonderingen die er niet hoort?
- Gebruiken je formulieren
@csrf? - Gebruiken state-changing acties POST, PUT, PATCH of DELETE, nooit GET?
Webhooks van derden slaan CSRF over, maar verifieer dan de handtekening van de afzender. Zie API-beveiliging bij e-commerce integraties voor dat patroon.
4. Rate limiting
Zonder limiet is je login-endpoint een gratis wachtwoord-tester. Definieer limieten in een service provider:
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip().'|'.$request->input('email'));
});
Koppel daarna throttle:login aan de route. Doe hetzelfde voor wachtwoord-reset, registratie, contactformulieren en dure API-endpoints. Zorg dat je trusted proxies kloppen (zie boven), anders limiteer je het IP van je load balancer.
5. Sessies en cookies
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax
secure: cookie gaat alleen over HTTPS.http_only: JavaScript kan de sessiecookie niet lezen. Dat beperkt de schade van een XSS.same_site: beperkt cross-site verzoeken.laxis een verstandige default.
Regenereer de sessie na login (Laravel doet dit in de standaard auth) en invalideer hem bij uitloggen. Overweeg een korte levensduur voor admin-omgevingen.
6. File uploads
Vertrouw nooit de bestandsnaam of de Content-Type van de client. Valideer met mimes: of mimetypes: en een maximale grootte. Sla bestanden op een private disk op en serveer ze via een gecontroleerde route of signed URL. Sla nooit uploads op in een map waar PHP wordt uitgevoerd.
Een uitgebreide aanpak, inclusief virusscan en opslag, staat in beveiligde file uploads in Laravel.
7. Dependencies
Je app is zo veilig als je zwakste package. Sinds Composer 2.4 bestaat composer audit. Dat vergelijkt je geinstalleerde packages met de security advisories van Packagist en geeft een niet-nul exitcode bij een treffer. Ideaal voor CI.
composer audit
Composer controleert ook tijdens install, update en require. Met --no-audit zet je dat uit, dus laat die vlag niet in je pipeline staan. Voor abandoned packages heeft Composer een audit.abandoned-instelling (ignore, report of fail). Controleer in de Composer-documentatie welke default jouw versie hanteert.
Aanvullend:
- Draai
composer auditin CI en laat de build falen. - Commit
composer.locken deploy metcomposer install --no-dev. - Plan een vast moment voor minor updates. Een achterstand van twee jaar maakt elke update een project.
- Draai
npm auditvoor je frontend-packages.
8. Secrets management
- Geen secrets in git, ook niet in oude commits. Is er een key gelekt? Roteer hem. Geschiedenis herschrijven is niet genoeg.
- Gebruik aparte keys per omgeving. Productie-keys staan niet op laptops.
- Beperk rechten van API-keys tot wat de integratie nodig heeft.
- Zet geen secrets in logs, foutmeldingen of frontend-bundles.
Voor teams helpt een secrets manager of de versleutelde .env die Laravel kent (env:encrypt). Kies wat bij je deployment past.
9. Security headers en CSP
Zet minimaal deze headers, via nginx of middleware:
Strict-Transport-Security(HSTS)X-Content-Type-Options: nosniffReferrer-PolicyContent-Security-PolicyX-Frame-Optionsofframe-ancestorsin je CSP
Een CSP is het lastigst, want inline scripts breken. Begin met Content-Security-Policy-Report-Only, bekijk de meldingen en schakel pas daarna over op afdwingen.
10. Logging zonder PII
Logs leven vaak langer dan de data zelf. Controleer:
- Staan wachtwoorden, tokens of volledige betaalgegevens in een log? Laravel houdt
passwordenpassword_confirmationuit flashed input viadontFlash. Voeg eigen gevoelige velden toe. - Log je volledige request-bodies? Doe dat niet voor formulieren met persoonsgegevens.
- Staat de log-retentie vast? Roteer en verwijder oude logs.
- Wie heeft toegang tot je log-aggregator?
Persoonsgegevens vallen onder de AVG. Dit is geen juridisch advies, maar log wat je nodig hebt voor debugging en niet meer.
11. Queues, Horizon, Telescope en andere dashboards
Debug-tools die op productie open staan zijn een klassiek lek.
- Horizon: toegang loopt via de
viewHorizongate. Buitenlocalis die standaard dicht. Vul hem met een lijst van e-mailadressen of rollen, en controleer dat hij echt werkt. Meer over beheer in Horizon queue monitoring. - Telescope: legt requests, queries en exceptions vast, inclusief data. Zet hem uit op productie (
TELESCOPE_ENABLED=false) of installeer hem alleen als dev-dependency. Gebruik anders deviewTelescopegate en filter gevoelige data. - Pulse, Ignition, debugbar: dezelfde regel. Niet in productie, of achter authenticatie.
Gate::define('viewHorizon', function (User $user) {
return in_array($user->email, ['[email protected]']);
});
Test na deploy: open /horizon en /telescope uitgelogd. Je wilt een 403 of 404.
12. Backups en deployment
Een backup die je nooit hebt teruggezet is een aanname.
- Maak automatische database-backups naar een andere locatie dan de server.
- Versleutel backups. Ze bevatten dezelfde data als productie.
- Test de restore, bijvoorbeeld elk kwartaal.
- Deploy via een script of Forge, niet met handmatige SSH-wijzigingen.
- Draai de webserver en PHP-FPM onder een gebruiker met minimale rechten.
storage/enbootstrap/cache/zijn schrijfbaar, de rest niet. - Houd OS en PHP up-to-date. Controleer op php.net welke PHP-versies nog security-updates krijgen.
- Zet SSH op keys, geen wachtwoorden, en een firewall voor alles behalve 80, 443 en je SSH-poort.
Volgorde: wat eerst?
Heb je weinig tijd? Begin hier, op volgorde van risico:
APP_DEBUG=false,.envniet bereikbaar.- Telescope en andere dashboards dicht.
- Policies op alle resources met een eigenaar.
- Rate limiting op login en reset.
composer auditin CI.- Cookie-flags en HTTPS.
- Raw queries en
{!! !!}nalopen.
Een externe audit vindt wat je zelf mist. Voor een overzicht van onze aanpak: security. Wil je sparren over de architectuur eronder, kijk dan bij Laravel maatwerk en API development.
Veelgestelde vragen
Is Laravel standaard veilig?
Deels. Laravel beschermt je tegen CSRF, SQL injection via bindings en XSS via Blade-escaping. Autorisatie, configuratie, rate limiting en dependency-beheer blijven jouw verantwoordelijkheid. De meeste incidenten ontstaan daar.
Wat is de belangrijkste instelling voor Laravel security?
APP_DEBUG=false op productie. Met debug aan lekt een fout je configuratie en code-paden. Controleer daarna dat .env niet via de webserver bereikbaar is.
Hoe controleer ik mijn Laravel dependencies op kwetsbaarheden?
Draai composer audit. Het commando vergelijkt je packages met bekende security advisories en geeft een niet-nul exitcode bij een treffer. Zet het in je CI-pipeline en draai npm audit voor frontend-packages.
Moet Telescope uit op productie?
In de regel ja. Telescope slaat requests, queries en exceptions op, inclusief gevoelige data. Zet hem uit met TELESCOPE_ENABLED=false, of beperk toegang strikt via de viewTelescope gate en filter gevoelige velden.
Hoe voorkom ik IDOR in Laravel?
Gebruik policies en authorize() bij elke resource met een eigenaar, of scope queries via de relatie van de ingelogde gebruiker ($user->orders()->findOrFail($id)). Schrijf tests die controleren dat gebruiker A niets van gebruiker B kan openen.
Laat je Laravel-applicatie nalopen
Wil je dat een senior developer meekijkt naar je Laravel-applicatie, of weten waar je staat voor je live gaat? Neem contact op. We bespreken je setup en wijzen aan wat eerst moet.

Geschreven door Ruthger Idema
15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.
Meer over ons team →


