Laravel

Laravel Security Hardening: A Production Checklist

Laravel ships with strong security defaults, but production breaches usually come from configuration, not the framework. Here is the checklist we run through before a Laravel application faces the internet.

Hardening is configuration, not a plugin

Laravel ships with stronger security defaults than most PHP frameworks. The Eloquent ORM parameterises queries, which shuts down SQL injection. Blade's {{ }} echo statements escape output with htmlspecialchars, which blocks the common cross-site scripting patterns. CSRF tokens are generated for state-changing requests. OWASP's Laravel cheat sheet puts it plainly: the framework is meant to be secure by default.

Yet when a Laravel application is breached, the framework is rarely the culprit. The usual causes are misconfiguration: debug mode left on, a shared or committed application key, an .env file the web server will happily serve, an upload endpoint with no size limit, or a login route with no rate limiting. Our Laravel development work treats hardening as a production configuration discipline — a checklist run before every release, not a one-off task.

This article is that checklist: the items we verify before a Laravel application faces the internet, grouped by area, with the reasoning attached so each item survives contact with a real project.

Run a version that still receives security fixes

No checklist compensates for an unpatched framework. Laravel's support policy provides bug fixes for 18 months and security fixes for 2 years after each major release. As of this writing, Laravel 11 reached the end of its security support on 12 March 2026, Laravel 12 receives security fixes until 24 February 2027, and Laravel 13 was released in the first quarter of 2026.

If your application runs on Laravel 10 or 11, the hardening work that matters most is the upgrade, not the configuration tweaks. An unsupported framework version means newly disclosed vulnerabilities will not be patched, however carefully you set your headers. Our guide to upgrading an older Laravel application walks through what that upgrade actually involves.

Once the framework is current, apply the same discipline to everything around it: the PHP runtime should be a supported release, and Composer dependencies should be scanned with composer audit so known-vulnerable packages surface before release, not after.

Lock down the production configuration

The first file to audit is .env. In production, APP_ENV must be production and APP_DEBUG must be false. A debug-mode error page can expose stack traces, internal paths and — in the worst cases — environment values, which makes this the single most common Laravel security mistake in production, and the first thing OWASP's cheat sheet flags.

Next, the application key. The php artisan key:generate command creates the key Laravel uses for cookie encryption, session data, signed URLs and password-reset tokens. It must never be committed to version control, never be shared across environments, and never be the development key copied into production. If a key is ever exposed, rotate it — every encrypted value it protects is then suspect.

Finally, check the deployment shape: run php artisan config:cache in production, confirm the web server's document root points at public/ and only public/, and verify with a direct request that the environment file cannot be served. Keep directory permissions at 775 at most and non-executable files at 664, and keep development tools such as Telescope and Debugbar out of production entirely — or gated behind a strict allowlist.

Harden authentication and sessions

Login, registration and password-reset routes are the most attacked endpoints on any application, so they should be the most defended. Apply rate limiting to every authentication route so brute-force and credential-stuffing attempts are slowed to a crawl before they reach your database.

Then harden the session cookies themselves. In config/session.php, keep http_only enabled so JavaScript cannot read the cookie, set same_site to lax (or strict) so the cookie is withheld from cross-site requests, and enable the secure flag so the cookie is only sent over HTTPS. Keep the session lifetime low — OWASP suggests 2–5 minutes of idle time for high-value applications and 15–30 minutes for low-risk ones.

For sensitive operations — changing an email address or password, deleting an account, exporting data — require password confirmation via Laravel's built-in password.confirm middleware. A hijacked session then cannot take over the account without the password, which turns a bad incident into a contained one.

Validate every input; keep mass assignment locked

Every request is untrusted until validated. Define validation rules for all incoming data and use $request->validated() rather than $request->all() when passing data to your models or services — validated data is the only data your application should act on.

Protect against mass assignment by declaring $fillable (or $guarded) on every Eloquent model, and never disable the protection with an empty $guarded array or an unguarded model. Avoid forceFill and forceCreate on anything derived from a request. The classic exploit is a user adding an is_admin field to a profile-update form; with strict fillables, the extra field is simply ignored.

The same distrust applies to the database layer. Eloquent parameterises standard queries, but the moment you reach for raw expressions, use bindings. And never let user input choose column names — a sort parameter passed straight into orderBy should always be validated against an explicit allowlist first.

Escape output; treat uploads as hostile

In Blade templates, {{ }} escapes output and is safe for ordinary text and quoted attributes; {!! !!} renders raw HTML and must never touch untrusted data. A comment field rendered with {!! !!} is a stored cross-site scripting vulnerability waiting for its first visitor.

File uploads deserve their own suspicion. Validate both type and size on every upload — the mimes rule for allowed formats and max for the size ceiling. Missing size validation invites storage exhaustion; missing type validation invites uploaded executables. Store uploads outside the web root, or on a disk that cannot execute them, so an uploaded PHP file can never be reached and run through the browser.

And never trust client-supplied metadata alone: filenames, extensions and MIME types all come from the uploader. Generate your own stored filenames server-side and treat every uploaded byte as hostile until your validation says otherwise.

Secure the HTTP layer

Encrypt everything in transit. Serve the application over HTTPS only, redirect plain HTTP to HTTPS, and set Strict-Transport-Security (HSTS) so browsers stop attempting insecure connections to your domain. Terminate TLS at the web server or proxy and make the redirect server-side, not optional.

Add security headers to every response: X-Frame-Options SAMEORIGIN (or a Content-Security-Policy frame-ancestors directive) to block clickjacking, a Content Security Policy that whitelists script sources, and X-Content-Type-Options nosniff. A small middleware in the global stack covers the whole application at once.

Where a web application firewall is available — Cloudflare's or your host's — use it as a second layer for the obvious attack patterns, not as a substitute for the configuration above.

Monitor, log and keep the discipline alive

Hardening decays. Dependencies drift, configuration changes with every deploy, and new team members do not know the checklist exists. Run composer audit in CI so vulnerable packages break the build instead of shipping, and check the framework's security-support window at least quarterly — especially as Laravel 12 approaches its February 2027 end of security fixes.

Log the security events that matter — logins, failed logins, logouts and password resets — to a dedicated channel at a sensible log level, and keep production logs free of debug noise. Logs you cannot read are decoration; logs that leak secrets are a second vulnerability.

Finally, give responsible reporters a way to reach you: a /.well-known/security.txt file with a security contact costs minutes and can turn a quiet disclosure into a fix instead of an incident. For applications handling sensitive data or payments, pair this checklist with a periodic external review — the checklist catches the common configuration mistakes, and a second pair of eyes catches the ones it cannot.

The pre-deploy checklist

Before a release, walk this list: framework and PHP on supported releases; composer audit clean; APP_ENV=production and APP_DEBUG=false; a unique, uncommitted APP_KEY; config:cache built; document root at public/ with .env unservable; directory permissions 775, files 664; Telescope and debug routes disabled or gated; authentication routes rate-limited; session cookies http_only, same_site lax, secure over HTTPS, short lifetime; password.confirm on sensitive actions; all inputs validated, models using $fillable, raw queries bound; Blade output escaped, uploads validated and stored non-executable; HTTPS enforced with HSTS and security headers in place; security events logged; security.txt published.

Work through it the same way every time — ideally as automated checks in your pipeline — and hardening stops being a heroic effort and becomes what production means. If you are choosing who does this work for you, our guide to choosing a Laravel development partner applies the same discipline to the selection: check the arrangement and the evidence, not just the claims.

Common Questions

Is Laravel secure out of the box?

Largely, yes. The framework parameterises database queries, escapes Blade output and generates CSRF tokens — the OWASP Laravel cheat sheet describes it as secure by default. The risk is in what developers and deployers change: debug mode left on, permissive settings, disabled protections, unvalidated uploads. Security reviews of Laravel applications almost always find configuration problems, not framework flaws.

What is the most common Laravel security mistake in production?

Leaving APP_DEBUG set to true. A debug error page can expose stack traces, internal file paths and environment details to anyone who triggers an error — and attackers trigger errors deliberately. It is also one of the easiest mistakes to prevent: set APP_DEBUG=false and APP_ENV=production, and verify the setting in your deployment pipeline rather than trusting memory.

How long does each Laravel version receive security fixes?

Two years from release, with bug fixes for 18 months, according to Laravel's official support policy. Laravel 11 reached the end of security support on 12 March 2026, Laravel 12 is covered until 24 February 2027, and Laravel 13 was released in the first quarter of 2026. If your version is past its window, upgrading is the single highest-value security task on your list.

Should we harden our old Laravel application or upgrade it first?

Do both, in that order of priority: upgrade first, then harden. Configuration improvements on an unsupported framework version still leave unpatched vulnerabilities in place, because security fixes are only released for supported versions. Once the application runs on a supported release, the checklist in this article applies in full. Our upgrade guide explains what the migration actually involves.

Is this checklist a substitute for a professional security audit?

No — it is the baseline an audit would expect to find. The checklist catches the common, high-frequency configuration mistakes that reviews flag first. For applications handling sensitive personal data, payments or regulated information, a periodic external review is worth the cost, because it finds the application-specific issues no checklist can. Publishing a security.txt contact also means independent researchers can report problems to you quietly.

Planning something similar?

Tell us about the problem and the outcome you need. We reply with questions and a sensible next step.

Discuss Your Project