Journal/ Security/ Laravel/ PHP
Security considerations for Laravel applications
A practical Laravel security checklist — authorisation, mass assignment, validation, rate limits, uploads, secrets, headers and payment callbacks.
Full-Stack Developer, Nepal
- Published
- Reading time
- 3 min read
Laravel gives you good security defaults: CSRF protection, hashed passwords, escaped Blade output and parameter binding in Eloquent. Most real-world vulnerabilities come from the code we add on top — a missing authorisation check, an over-permissive model, an upload that trusts the file name.
This is the checklist I work through on applications that handle user accounts, orders and payments.
1. Authorise every action, not just every page
The most common serious bug in web applications is broken access control: user A can see or change user B’s data by changing an ID in the URL.
Authentication (“who are you?”) isn’t enough. Every action needs authorisation (“are you allowed to do this to this record?”). Policies keep that logic in one place:
class OrderPolicy
{
public function view(User $user, Order $order): bool
{
return $order->user_id === $user->id;
}
}
public function show(Order $order)
{
$this->authorize('view', $order);
return view('orders.show', compact('order'));
}
Write a feature test that proves one user cannot open another user’s order. It’s a small test that protects against a very expensive bug.
Better still, scope queries to the current user so other people’s records are never loaded at all:
$order = $request->user()->orders()->findOrFail($id);
2. Guard against mass assignment
Model::create($request->all()) lets a user send fields you never intended — is_admin, balance, status. Always pass validated data and define $fillable explicitly:
protected $fillable = ['name', 'email', 'phone'];
$user->update($request->validated());
3. Validate everything at the edge
Use Form Request classes so validation can’t be forgotten:
public function rules(): array
{
return [
'quantity' => ['required', 'integer', 'min:1', 'max:20'],
'product_id' => ['required', Rule::exists('products', 'id')->where('is_active', true)],
];
}
Validate types, ranges and existence — not only “required”. Never trust prices, totals or discounts sent from the browser; recalculate them on the server.
4. Rate-limit sensitive endpoints
Login, password reset, OTP verification, contact forms and search are common targets for brute force and abuse:
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->input('email').'|'.$request->ip());
});
Route::post('/login', LoginController::class)->middleware('throttle:login');
5. Handle file uploads defensively
- Validate by MIME type and size:
'image' => ['required', 'image', 'mimes:jpg,png,webp', 'max:5120']. - Generate your own file names — never use the client-provided name as a path.
- Store private files outside
public/and serve them through a controller that checks authorisation. - Re-encode images with a library like Intervention Image. It also strips embedded metadata.
- Make sure the upload directory can never execute PHP.
6. Keep secrets out of the repository
.envis in.gitignore— confirm it has never been committed. If it has, rotate every key in it.- Commit an
.env.examplewith placeholder values. - Use different keys for local, staging and production.
- Never expose
APP_DEBUG=truein production; error pages can reveal environment variables. - Keep API keys on the server. Anything shipped to the browser is public.
7. Treat payment callbacks as untrusted
Payment gateway callbacks and webhooks are just HTTP requests — anyone can send one. Before marking an order as paid:
- Verify the signature, or confirm the transaction by calling the gateway’s verification API from your server.
- Check that the amount and currency match the order.
- Make processing idempotent: store the gateway’s transaction reference with a unique constraint so a repeated callback can’t deliver twice.
- Log the raw payload for later investigation.
8. Escape output, and be careful with raw HTML
Blade’s {{ }} escapes output. {!! !!} doesn’t. Only use raw output for HTML you generated yourself or have sanitised with a proper HTML purifier. The same applies in JavaScript: prefer textContent over innerHTML for user data.
9. Send security headers
At the web server or middleware level:
Strict-Transport-Securityto enforce HTTPSX-Content-Type-Options: nosniffX-Frame-Options: DENYor a CSPframe-ancestorsruleReferrer-Policy: strict-origin-when-cross-origin- A
Content-Security-Policy, starting strict and relaxing only where needed
10. Keep dependencies current
Run composer audit and npm audit regularly and before each release. Keep Laravel and PHP on supported versions — security fixes stop arriving for old ones.
11. Log what matters
Log authentication failures, permission denials, payment events and admin actions. You can’t investigate an incident you have no record of. Just don’t log passwords, full card data or tokens.
Security is a habit
None of these steps are difficult. The risk is in skipping one “just for now”. Building them into your defaults — policies on every model, form requests on every write, rate limits on every sensitive route — makes secure code the path of least resistance.