By framework
Laravel security — a production hardening reference
A Laravel production-hardening reference: a priority-ordered checklist, the dangerous defaults, and per-area guidance from secrets and config to authorization and dependency CVEs, plus a self-verification checklist. Defensive, no attack steps.
For: anyone running Laravel in production. No attack steps here — this is a working reference for hardening: a priority-ordered checklist, the dangerous defaults, per-area guidance, and self-verification. For the cross-framework picture, see the security-by-framework hub.
Priority-ordered hardening checklist
Do this table top-down. P0 is a prerequisite for the rest, P1 is the most frequent source of incidents, P2 is ongoing operational hygiene.
P0 ── Prerequisite (do first)
Debug off in prod / secrets off the public surface at perms 600 / manage APP_KEY
P1 ── Top source of incidents
Authorization (Policy/Gate) / Mass Assignment control / dependency CVEs / prod caching
P2 ── Operational hygiene
Sessions/CSRF/cookies / upload validation / HTTPS, headers, rate limiting
| Priority | Control | Specifics (Laravel) |
|---|---|---|
| P0 | Disable production debug | APP_DEBUG=false / APP_ENV=production, pinned with config:cache. Don't leak internals on the error page |
| P0 | Secrets out of the public surface | .env, backups, keys outside public/, perms 600. storage/logs not public |
| P0 | Manage APP_KEY safely | Underpins encryption, signed cookies, sessions. Inject from env; rotate on leak |
| P1 | Explicit authorization | Policy / Gate + authorize() / can middleware, scoped by owner/permission |
| P1 | Control Mass Assignment | Declare $fillable; avoid bulk-assigning $request->all(), use validated() |
| P1 | Monitor Composer CVEs | composer audit / osv-scanner; judge by running version, patch fast |
| P1 | Production caching | config:cache route:cache view:cache for reliable settings + speed |
| P2 | Session/cookie safety | Set secure http_only same_site; regenerate the session on login |
| P2 | Upload validation | Validate type/size, store outside public/, no execute permission |
| P2 | HTTPS/headers/rate limiting | Force HTTPS, HSTS, throttle on login/API |
1. Secrets and APP_KEY (P0)
Laravel keeps .env outside the document root (at the project root) by default. Incidents mostly come from putting secrets where they don't belong.
- Never place
.env, DB dumps, backups, or key files inpublic/. Keep secrets outside the app root at perms 600 (owner-only). - Don't expose
storage/orstorage/logs/(logs can contain secrets or personal data). Don't commit.envto the repo. APP_KEYunderpins encryption, signed URLs, encrypted cookies, and sessions. Inject it from the environment and rotate promptly if leaked (note that this invalidates existing encrypted data/sessions).
For the general principle see keep secrets out of public directories; for a real full-exposure case see a full .env exposure.
2. Production config: DEBUG, environment, caching (P0)
APP_DEBUG=false / APP_ENV=production
Pin config with caching
php artisan config:cache (+ route:cache view:cache) makes settings apply reliably and speeds the app. Note: after config:cache, env() outside config/ returns null, so reference values via config('...') in the app.Don't expose diagnostic tools in production
3. Authorization: Broken Access Control (P1 — the top source)
The most common production incident is "authenticated but not authorized." Being able to log in doesn't mean being allowed to do the action.
Common (dangerous)
- no authorization — "logged in = can view/update"
- route-model binding fetches another user's ID
Model::create($request->all())accepts every field- a privilege field like
is_adminassigned from user input
Correct
- Policy / Gate +
authorize()/canmiddleware, scoping owner/permission every time - reads too are owner-scoped (e.g.
where('user_id', $me)) - limit input with
$request->validated()(FormRequest) - declare
$fillableto block Mass Assignment
See what IDOR is. The key is an ownership check on every read/update/delete path.
4. Injection and output
Laravel's Eloquent/query builder binds values with placeholders, and Blade escapes output by default. The rule is don't disable that safety yourself.
- SQL: don't mix user input into
whereRaw/DB::rawby string concatenation. Even when raw SQL is needed, use bindings (→ what SQL injection is). - XSS: Blade
{{ }}escapes;{!! !!}is raw output. Don't pass user input to{!! !!}. If you must emit HTML, only after sanitizing (→ what XSS is). - Validation: validate type/range/allowed values with FormRequest /
validate()before use.
5. Sessions, CSRF, cookies (P2)
- CSRF: don't globally disable the web middleware's CSRF protection. Use
@csrfin forms and proper token handling for SPAs (→ what CSRF is). - Cookies/sessions: set
secure(HTTPS),http_only,same_siteinconfig/session.php. Regenerate the session on login to prevent fixation (Laravel's auth regenerates). - Brute force: apply
throttle/ login-attempt limits to login.
6. File uploads and public files (P2)
- Validate mime type, extension, and size, and don't trust the client-supplied filename.
- Store outside
public/(e.g.storage/) with no execute permission. Serve only what needs to be public, via a controlled path. - Put authorization on delivery too, so sequential direct links can't fetch another user's file.
7. HTTPS, security headers, rate limiting (P2)
- Force HTTPS (
URL::forceScheme('https'), etc.) + HSTS. Behind a load balancer, useTrustProxiesso the scheme is detected correctly. - Security headers (X-Content-Type-Options, etc.; design CSP around your asset setup). Check your own site with the security headers checker.
- Rate limiting: apply
throttleto login, APIs, and password reset.
8. Dependencies and versions (P1)
- Monitor Composer dependency CVEs with
composer auditor osv-scanner, judge by the running version, and patch fast (→ monitoring dependency CVEs · the vulnerability-response playbook). - Keep Laravel and PHP on supported versions. Don't leave EOL versions in place (unfixable holes accumulate).
Verify: is your Laravel actually hardened?
Building it isn't the end — it's done only once you've checked. These are defensive self-checks against your own site/environment.
Secrets aren't fetchable by URL
/.env and /storage/logs/laravel.log and confirm they return 404 (if fetchable, fix immediately and rotate keys).No debug exposure in production
Authorization holds
Cookies/sessions and dependencies
composer audit is clean.This site's view: even with strong defaults, authorization and dependencies are on you
Laravel has many good defaults, but authorization — who may do what — and dependency freshness are app- and operation-specific, so no framework can guard them automatically. The incidents we keep seeing are less elaborate attacks than settings/operations patterns: "authenticated but no ownership check," "debug open in production," "a secret exposed." So the center of gravity is working the table above top-down and doing the unglamorous thing — scope every read/update by owner, and monitor dependencies for CVEs.
Read next
- Hub: security by framework · Next.js security
- Secrets: keep secrets out of public directories · Case: a full .env exposure
- Authorization / practice: what IDOR is · the vulnerability-response playbook · monitoring dependency CVEs
- Glossary: SQL injection · XSS · CSRF
FAQ
QWhat should I do first to secure Laravel?
The three P0 items: (1) ensure APP_DEBUG=false in production and pin the config with config:cache; (2) keep .env and secret files out of the public directory at tight permissions; (3) manage APP_KEY safely (it underpins encryption, signed cookies, and sessions, so rotate on leak). These are prerequisites for everything else. Next, move to authorization (Policy/Gate) and Composer dependency CVE monitoring.
QWhat's dangerous about shipping with APP_DEBUG=true?
With debug on, the exception page can show not just a stack trace but internals like config values, environment variables, and connection info. An attacker can trigger errors on purpose to extract them. In production, set APP_DEBUG=false and APP_ENV=production and make it stick with config:cache. Also don't expose diagnostic tools like Telescope or Debugbar in production.
QWhy does env() return null after config:cache?
config:cache compiles your config into one file for speed, and afterward env() called outside the config files returns null (only the .env at cache time is read). The fix is to use env() only inside config/ and reference values in the app via config('...'). In production, caching config/route/view is the norm — it makes settings apply reliably and speeds the app up.
QHow do I prevent 'logged in means allowed'?
Authentication (login) and authorization (whether the action is allowed) are different. In Laravel, implement Policy/Gate and use authorize() or the can middleware to scope every read/update/delete by owner or permission. Also control Mass Assignment by declaring $fillable and avoid bulk-assigning $request->all(). Without this, swapping an ID in the URL reaches another user's data (IDOR).
QHow do I manage Composer dependency vulnerabilities?
Machine-monitor known CVEs with composer audit or osv-scanner, judge by the running version, and patch fast (decide on what's actually installed, not the composer.json declaration). Also keep Laravel and PHP on supported versions and don't leave EOL versions in place. Dependency freshness is a more realistic factor in incidents than elaborate attacks.