EthicsComputerCourses
Join training +91 90532 10052

Laravel Security Checklist

Laravel · Lesson 25 of 30 Advanced 9 min read Updated

By the end of this lesson you will have a checklist you can run through before any Laravel app goes live, and you will understand the attack behind each item. A coaching app holds children's names, phone numbers and fee records, so a leak hurts real families. Laravel gives you strong defaults; most breaches happen when a developer switches one of them off.

Think like a careful bank

A bank branch does not rely on one lock. It has a guard at the door, ID checks at the counter, a locked cash room, CCTV and an audit at the end of the day. Security in web apps works the same way: several layers, so one mistake does not open everything. This is called defence in depth.

Each section below is one layer. The table at the end is the checklist.

1. Configuration and secrets

  • APP_DEBUG=false in production. You saw in Middleware & Sessions that a debug error response lists full file paths and a stack trace. In production Laravel returns only a short message such as {"message": "The fee counter is open from 6:00 to 8:00 IST."}.
  • APP_ENV=production. Some protections, such as asking for confirmation before migrate runs, depend on it.
  • Keep APP_KEY secret and unique per environment. It encrypts cookies, sessions and encrypted casts. If it leaks, an attacker may forge cookies. If you must rotate it, Laravel supports APP_PREVIOUS_KEYS so old data can still be decrypted.
  • Never commit .env. Keep secrets in .env on the server, read through config().
  • Serve only public/. The web root must be the public folder, so .env, storage and vendor cannot be downloaded.

2. CSRF: forged form submissions

Cross-site request forgery tricks a logged-in user's browser into submitting a form to your app from another site. Laravel blocks it with a token in every form (@csrf). A POST without the token got 419 in Blade Templates.

Laravel 13 strengthened this layer. The middleware is now called PreventRequestForgery, and besides the token it also checks the browser's Sec-Fetch-Site header to tell whether a request came from your own site. Do not remove it from the web group, and exclude routes from it only for real reasons, such as a payment gateway's webhook, which you then verify with the gateway's signature instead:

PHP
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(except: [
        'webhooks/payment-gateway',
    ]);
})

3. XSS: injected scripts

Cross-site scripting happens when user input is shown as HTML and a <script> runs in other users' browsers. Blade's {{ }} escapes output, as you saw when <b>Last date</b> came out as &lt;b&gt;. The rule is simple:

HTML
{{ $student->name }}            {{-- safe: escaped --}}
{!! $student->remarks !!}       {{-- dangerous if a user typed it --}}

If you must allow some formatting (for example, notes written in a rich text editor), clean the HTML with a well-maintained sanitiser library before saving or printing it.

4. SQL injection

Eloquent and the query builder use prepared statements, so values never mix with SQL text. The danger comes back with raw methods:

PHP
// Dangerous: user input glued into SQL
DB::select("select * from students where city = '{$request->city}'");

// Safe: placeholder and bindings
DB::select('select * from students where city = ?', [$request->city]);
Student::where('city', $request->city)->get();

Also never pass user input as a column name, for example orderBy($request->sort). Check it against an allow-list first: in_array($sort, ['name', 'city'], true).

5. Mass assignment

Only list safe columns in #[Fillable] (or $fillable), and save $request->validated(), not $request->all(). You saw in Validation & Form Requests that validated() dropped a sneaky is_admin field. Columns like role, is_admin, balance or status should be set deliberately in code, not from a form.

6. Authentication and authorisation

  • Hash passwords (the hashed cast does it), and never store or email plain passwords.
  • Rate-limit login, OTP and token routes with throttle.
  • Regenerate the session after login; Laravel's own login code does this.
  • Check permissions on the server with policies and gates for every action, as in Authorisation. The classic bug is IDOR (insecure direct object reference): /receipts/1005 works, so the user tries /receipts/1004 and sees another family's receipt. A policy check on every model lookup stops it.
  • Offer two-factor authentication for staff accounts; the starter kits include it.

7. Files and uploads

  • Validate type and size with image, mimes and max.
  • Store with generated names (store()), never the user's file name.
  • Keep private documents on the private local disk and serve them through a controller that checks permission.
  • Never let users upload .php files into public/.

8. Sessions, cookies and serialisation

New Laravel 13 projects set the session serialization option to json and the cache serializable_classes option to false. Both stop attackers who somehow get your APP_KEY from turning stored PHP objects into code execution (a deserialisation attack). If you upgrade an older app, consider adopting these settings.

On HTTPS sites set SESSION_SECURE_COOKIE=true so session cookies are never sent over plain HTTP. The session cookie is already httponly (JavaScript cannot read it) and samesite=lax, as the real headers in Project Structure showed.

9. HTTPS and security headers

Serve every page over HTTPS (free certificates from Let's Encrypt). Add a few headers in your web server or in a small middleware:

PHP
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', 'SAMEORIGIN');
    $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');

    return $response;
}

A Content Security Policy (CSP) header goes further by listing which scripts may run; it takes some tuning, so add it once the app is stable.

10. Dependencies and updates

Laravel ships security fixes in small releases, sometimes weekly. Composer can warn you about known vulnerable packages:

Terminal
composer audit
Output
No security vulnerability advisories found.

That exact line appeared at the end of our own composer create-project. Update regularly with composer update, run your tests, and upgrade the major version before its security support ends (see Upgrading Old Laravel Apps).

11. Personal data and the law

Student records are personal data. India's Digital Personal Data Protection Act, 2023 (DPDP Act) sets duties for anyone who processes it, including stricter rules for children's data, such as needing verifiable consent from a parent. Collect only what you need, protect it, delete it when no longer required, and ask a legal adviser what applies to your app. Log access to sensitive records, so you can answer "who saw this?".

The checklist

AreaCheck before going live
ConfigAPP_ENV=production, APP_DEBUG=false, unique secret APP_KEY, .env not in Git
ServerWeb root is public/; HTTPS everywhere; SESSION_SECURE_COOKIE=true
Forms@csrf in every form; forgery protection not removed; webhooks verified by signature
Output{{ }} everywhere; any {!! !!} reviewed
DatabaseNo user input in raw SQL strings or column names
ModelsOnly safe columns fillable; save validated() data
Accessauth on private routes; policies on every model action; throttle on login and APIs
FilesValidated uploads, generated names, private disk for documents
Updatescomposer audit clean; Laravel version still receiving security fixes
DataOnly needed personal data stored; access logged; backups encrypted

Common mistakes

  • Turning on debug mode "just for a minute" on the live site. Use logs instead.
  • Disabling CSRF for a whole route group because one AJAX call failed. Send the token from JavaScript instead.
  • Checking permissions only in Blade. Hidden buttons are not security.
  • Trusting $request->all(). Validate and whitelist.
  • Never updating. An unpatched old version is an open door.

Key points

  • Production needs APP_DEBUG=false, a secret APP_KEY, HTTPS and public/ as the only web root.
  • Laravel blocks CSRF (PreventRequestForgery), XSS ({{ }}) and SQL injection (bindings) by default; do not switch these off.
  • Fillable lists, validated() data and policies on every model action stop mass assignment and IDOR bugs.
  • Validate uploads, keep private files private, and rate-limit logins and APIs.
  • Run composer audit, update often, and handle students' personal data with care under the DPDP Act.

Exercise

Review this controller method and list every security problem you can find. Then rewrite it safely.

PHP
public function receipt(Request $request)
{
    $p = DB::select("select * from payments where receipt_no = '".$request->no."'")[0];
    return view('receipt', ['note' => $request->note, 'payment' => $p]);
}

The view prints the note with {!! $note !!}.

Show solution

Problems: (1) SQL injection, because $request->no is glued into the SQL; (2) no authorisation, so anyone can read any receipt (IDOR); (3) XSS, because the note is printed raw; (4) no validation and no 404 when nothing is found (the [0] crashes).

PHP
public function receipt(Request $request): View
{
    $data = $request->validate([
        'no' => ['required', 'string', 'max:20'],
        'note' => ['nullable', 'string', 'max:200'],
    ]);

    $payment = Payment::where('receipt_no', $data['no'])->firstOrFail();

    Gate::authorize('view', $payment);

    return view('receipt', ['note' => $data['note'] ?? '', 'payment' => $payment]);
}

In the view, print {{ $note }}. Eloquent binds the receipt number safely, firstOrFail() returns 404, and the policy decides who may see the receipt.

Quick Quiz

1. Why must APP_DEBUG be false on a live site?

2. Which Blade syntax prints a value without escaping it?

3. A user changes /receipts/1005 to /receipts/1004 and sees someone else's receipt. What is missing?

4. What does composer audit do?

Learn Laravel with a mentor

Free lessons get you started. Our PHP & MySQL Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call