Skip to content
>_ITDITDWeb Security Platform

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.

Published 2026-07-02 Updated 2026-07-02 8 min read

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

Harden from the foundation up: P0 (prerequisite) → P1 (top source of incidents) → P2 (operational hygiene).
PriorityControlSpecifics (Laravel)
P0Disable production debugAPP_DEBUG=false / APP_ENV=production, pinned with config:cache. Don't leak internals on the error page
P0Secrets out of the public surface.env, backups, keys outside public/, perms 600. storage/logs not public
P0Manage APP_KEY safelyUnderpins encryption, signed cookies, sessions. Inject from env; rotate on leak
P1Explicit authorizationPolicy / Gate + authorize() / can middleware, scoped by owner/permission
P1Control Mass AssignmentDeclare $fillable; avoid bulk-assigning $request->all(), use validated()
P1Monitor Composer CVEscomposer audit / osv-scanner; judge by running version, patch fast
P1Production cachingconfig:cache route:cache view:cache for reliable settings + speed
P2Session/cookie safetySet secure http_only same_site; regenerate the session on login
P2Upload validationValidate type/size, store outside public/, no execute permission
P2HTTPS/headers/rate limitingForce 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 in public/. Keep secrets outside the app root at perms 600 (owner-only).
  • Don't expose storage/ or storage/logs/ (logs can contain secrets or personal data). Don't commit .env to the repo.
  • APP_KEY underpins 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)

1

APP_DEBUG=false / APP_ENV=production

Turn debug off in production. On, the exception page leaks env vars and connection info, extractable via deliberate errors.
2

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.
3

Don't expose diagnostic tools in production

Disable or access-restrict Telescope / Horizon / Debugbar in production. Keep detailed errors and stack traces off the public surface.

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_admin assigned from user input

Correct

  • Policy / Gate + authorize() / can middleware, scoping owner/permission every time
  • reads too are owner-scoped (e.g. where('user_id', $me))
  • limit input with $request->validated() (FormRequest)
  • declare $fillable to 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::raw by 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 @csrf in forms and proper token handling for SPAs (→ what CSRF is).
  • Cookies/sessions: set secure (HTTPS), http_only, same_site in config/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, use TrustProxies so 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 throttle to login, APIs, and password reset.

8. Dependencies and versions (P1)

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.

1

Secrets aren't fetchable by URL

On your own domain, hit /.env and /storage/logs/laravel.log and confirm they return 404 (if fetchable, fix immediately and rotate keys).
2

No debug exposure in production

Trigger an error (e.g. a non-existent route) and confirm a generic error page shows — no env vars or stack traces.
3

Authorization holds

In a test environment, request another user's resource ID and confirm it is denied (on read, update, and delete paths).
4

Cookies/sessions and dependencies

Confirm the session cookie carries Secure/HttpOnly/SameSite, and that 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.

FAQ

QWhat should I do first to secure Laravel?
A

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?
A

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?
A

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'?
A

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?
A

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.