Laravel Security Checklist
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=falsein 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 beforemigrateruns, depend on it.- Keep
APP_KEYsecret and unique per environment. It encrypts cookies, sessions andencryptedcasts. If it leaks, an attacker may forge cookies. If you must rotate it, Laravel supportsAPP_PREVIOUS_KEYSso old data can still be decrypted. - Never commit
.env. Keep secrets in.envon the server, read throughconfig(). - Serve only
public/. The web root must be thepublicfolder, so.env,storageandvendorcannot 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:
// 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 <b>. The rule is simple:
{{ $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:
// 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
hashedcast 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/1005works, so the user tries/receipts/1004and 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,mimesandmax. - Store with generated names (
store()), never the user's file name. - Keep private documents on the private
localdisk and serve them through a controller that checks permission. - Never let users upload
.phpfiles intopublic/.
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:
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:
composer auditNo 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
| Area | Check before going live |
|---|---|
| Config | APP_ENV=production, APP_DEBUG=false, unique secret APP_KEY, .env not in Git |
| Server | Web 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 |
| Database | No user input in raw SQL strings or column names |
| Models | Only safe columns fillable; save validated() data |
| Access | auth on private routes; policies on every model action; throttle on login and APIs |
| Files | Validated uploads, generated names, private disk for documents |
| Updates | composer audit clean; Laravel version still receiving security fixes |
| Data | Only 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 secretAPP_KEY, HTTPS andpublic/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.
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).
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?
Detailed errors are for developers only.
2. Which Blade syntax prints a value without escaping it?
{!! !!} prints raw HTML and must never be used for user input.
3. A user changes /receipts/1005 to /receipts/1004 and sees someone else's receipt. What is missing?
This is an IDOR bug; check permission for every record.
4. What does composer audit do?
It checks your dependencies against a list of known vulnerabilities.
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
