Web Development 5-8 minutes

Laravel 13 Production Security Checklist for 2026

Diego Cortés
Diego Cortés
Full Stack Developer & SEO Specialist
Share:
Laravel 13 Production Security Checklist for 2026
Image generated with AI

Your Laravel app works: login succeeds, routes respond... but security is not a switch you flip, it is a stack of layers — .env, headers, XSS, CSRF, dependencies — to review before and after deploy. This is the 2026 production security checklist for Laravel 13.

Why Laravel Security Is Reviewed in Layers

Web application security is never solved with a single measure: it is built in layers, so that if one fails, the next still protects you. In Laravel, each layer has a name — configuration, transport, headers, validation, authentication, dependencies — and this guide walks through them in the same order you would review a deployment.

Secure by Default, Insecure by Configuration

Laravel is secure by default in many of its parts: Blade escapes output, Eloquent uses parameterized queries, and forms carry CSRF tokens. The problem appears when configuration breaks that security: APP_DEBUG left on in production, a model without $fillable, or a controller trusting raw input. The vulnerability is almost never in the framework; it is in the layer you configured badly.

The OWASP Top 10 and the Protections Laravel Already Ships

The OWASP Top 10 describes the most exploited web vulnerabilities, and the OWASP cheat sheet series includes one specifically for Laravel. The good news: SQL injection, XSS, and CSRF — three Top 10 classics — have native mitigations in the framework. The real work is enabling them properly and combining them with validation, access control, and dependency auditing.

Environment and Configuration First

APP_ENV=production and APP_DEBUG=false

The first thing to review in any deploy is the environment. In the production .env, APP_ENV must be production and APP_DEBUG must be false. With APP_DEBUG on, any error shows Laravel's detailed error page with internal routes, environment variables, and stack traces that reveal the app's architecture; in production that is information that should never leave the server.

An APP_KEY and Secrets Outside the Repository

The APP_KEY encrypts sessions and sensitive data: if it does not exist or was generated with a predictable value, encryption is useless. Generate it with php artisan key:generate and use a different key per environment. Secrets — API keys, database credentials, tokens — live in the .env, which must never be committed: keep .env in .gitignore and configure it on the server outside version control.

Permissions for .env and storage

Only the server user that runs PHP needs to read the .env. Check that it is not readable by the public web server user, and that storage has the minimum permissions for the app to write logs and uploaded files. A .env served as a static file is a secret leak in a single request.

HTTPS and Secure Cookies

Forcing HTTPS and SESSION_SECURE_COOKIE

The whole app must be served over HTTPS, and session cookies should only travel over that channel. In the production .env: SESSION_SECURE_COOKIE=true. The browser then refuses to send the session cookie over HTTP, making session hijacking on an intermediate network much harder. On the server, redirect HTTP to HTTPS and configure TrustProxies if the app sits behind a proxy or load balancer.

HSTS: Make the Browser Come Back over HTTPS Only

Beyond redirecting, declare the Strict-Transport-Security (HSTS) policy with a header: the browser remembers that this domain is only ever spoken over HTTPS and does not even try the insecure version. Add it with your own middleware or a security-headers library, pointed at the exact domain with a reasonable max-age.

HTTP Security Headers

X-Content-Type-Options and X-Frame-Options (Clickjacking)

Two small headers close classic holes. X-Content-Type-Options: nosniff stops the browser from guessing a resource's type (MIME sniffing) and executing scripts disguised as images or CSS. X-Frame-Options (or frame-ancestors in the CSP) prevents another site from embedding your app in an iframe and running a clickjacking attack.

Content-Security-Policy to Mitigate XSS

The Content-Security-Policy (CSP) restricts the origins from which the browser may load scripts, styles, and images. It is a second line against XSS: even if an attacker manages to inject a script tag, the CSP blocks its execution if the origin is not allowed. A strict policy is configured incrementally: start in monitoring mode and tighten gradually so you do not break your own front end.

A Five-Line Custom Headers Middleware

In Laravel you can group these headers in your own middleware, just a few lines, and apply it to the web routes:

public function handle(Request $request, Closure $next): Response
{
    $response = $next($request);

    $response->headers->set('X-Content-Type-Options', 'nosniff');
    $response->headers->set('X-Frame-Options', 'DENY');
    $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
    $response->headers->set('Content-Security-Policy', "default-src 'self'");

    return $response;
}

Register it in the web group and confirm with the browser developer tools that the headers arrive on every response.

The Code That Handles User Input

XSS: Blade {{ }} Escapes, {!! !!} Only with Trusted HTML

Blade escapes output by default: {{ $variable }} converts dangerous characters into entities, so an attempted script renders as harmless text. The risk comes with {!! $variable !!}, which prints raw HTML. Use it only when you know the content is trusted or has been sanitized first; for user-generated HTML, run it through a sanitizer before printing.

Validation: Form Requests with Explicit Rules

Every action that receives user input must validate it with explicit rules. The clean way in Laravel is a Form Request class with its rules method: define what each field expects, with type, format, size, and limits. Never trust $request->all() to assign to a model without passing validation and the assignable-field controls first.

public function rules(): array
{
    return [
        'name'   => ['required', 'string', 'max:255'],
        'email'  => ['required', 'email', 'max:255', 'unique:users,email'],
        'avatar' => ['required', 'image', 'mimes:jpg,png,webp', 'max:2048'],
    ];
}

Mass Assignment: $fillable and $guarded on Every Model

Mass assignment happens when you assign many fields to a model at once — for example from the user's complete input — and a field that should not change (a role, a balance, an admin flag) sneaks into the assignment. You control it by declaring on each model which fields are assignable ($fillable) or, conversely, which are locked ($guarded). If a model has neither, ask why: that is the exception you need to justify.

SQL Injection: Parameterized Queries and Care with whereRaw

Eloquent and the query builder protect against SQL injection because they bind values as parameters instead of concatenating them. The danger appears when you concatenate user input inside a whereRaw or DB::raw. If you need raw SQL, pass the values as bindings, never interpolated into the string.

// Safe: the query builder escapes the value as a parameter
$users = User::where('email', $request->input('email'))->get();

// If you must use whereRaw, pass bindings, do not concatenate
$users = User::whereRaw('email = ?', [$request->input('email')])->get();

Authentication and Sessions

CSRF: The Automatic Token and When Not to Disable It

Laravel protects forms with an automatic CSRF token: every browser write request must carry it or the framework rejects the request. The temptation to disable it — in the VerifyCsrfToken middleware or by forgetting @csrf — appears with odd integrations, but it is almost never the right fix. Keep the token on every form and use API routes with their own authentication when the client is not a browser form.

Rate Limiting on Login

A login form without limits is an open door to brute-force password attempts. Laravel ships rate limiting out of the box: apply a throttle to your login routes to cap attempts per IP or per user within a time window. The blog has a full rate limiting tutorial for Laravel 13 if you need the configuration details.

Second Factor: 2FA/TOTP and Passkeys

If the app stores customer or business data, a password alone is not enough. A second factor — a TOTP app like Google Authenticator or passkeys — blocks access even when a password leaks. You do not need to build it by hand: mature packages exist, and the blog has step-by-step 2FA/TOTP and passkey guides for Laravel 13.

APIs: Sanctum and CORS

Sanctum Tokens with Least-Privilege Scopes

For your own API, Sanctum issues tokens with scopes: each token should only be able to do what its holder needs. Define minimal scopes per client type and review them the way you would review user permissions. A token with full access on a client that only reads a catalog is an unnecessary attack surface.

CORS: Only the Origins That Actually Consume Your API

The CORS configuration decides which browser origins may call your API. Keep the permissive default only during development; in production, list only the domains that genuinely consume the API. Open CORS turns your API into a resource any website can call from an authenticated user's browser.

Secure File Uploads

File uploads are validated on two fronts. First, the content: MIME type, extension, maximum size, and — when the file claims to be an image — verifying that it actually is, because a fake PNG can hide a script. Second, the storage: keep files outside the public root and serve them through a controlled route or a private disk, so they can never execute as code on your domain. The blog has a dedicated file uploads and storage post for Laravel 13.

Dependencies: composer audit Locally and in CI

What composer audit Reports and How to Read It

Vulnerabilities also arrive through dependencies: a package with a known flaw can open up the app even when your code is impeccable. Composer includes the composer audit command, which queries Packagist security advisories and lists affected packages, the vulnerable version, and the fixed version. Run it locally before every release:

composer audit

When it reports something, the reading is direct: update the package to the fixed version and repeat until the output is clean. If a dependency has no fix available, evaluate replacing it or isolating its usage.

Automating with Dependabot and Your CI Pipeline

Running composer audit by hand gets forgotten. Automate it: add the command to your CI pipeline so a deploy fails when there are known vulnerabilities, and enable Dependabot to receive pull requests when a dependency ships a security fix. That turns dependency auditing into part of the flow instead of a task that never gets done.

Final Checklist: 10 Points for Production

To close, here are ten verifiable points you can review in an afternoon before every deploy:

  1. APP_ENV=production and APP_DEBUG=false in the server .env.
  2. APP_KEY generated and unique per environment; secrets outside the repository.
  3. The whole app over HTTPS, with SESSION_SECURE_COOKIE=true and HSTS declared.
  4. Security headers present: X-Content-Type-Options, X-Frame-Options, and CSP.
  5. Blade output uses {{ }}; {!! !!} only with trusted, sanitized HTML.
  6. Form Requests validate every action that receives input.
  7. $fillable or $guarded defined on every Eloquent model.
  8. CSRF active on forms, rate limiting on login, and 2FA where sensitive data lives.
  9. API uses least-privilege Sanctum tokens and CORS restricted to your origins.
  10. composer audit is clean, run locally and automated in CI.

Conclusion

Security in Laravel is not a state you reach one day: it is a periodic review of layers the framework already gives you nearly solved — Blade escapes, Eloquent parameterizes, CSRF protects — and that only break through bad configuration. Run the checklist before every deploy, automate what you can with composer audit and CI, and treat security as part of development, not a final chore. If this guide helped, the blog has in-depth tutorials on rate limiting, 2FA, Sanctum, and VPS deployment with Laravel 13.

Categories