Laravel is fast, powerful, and elegant — but are your apps secure?
Whether you're a junior developer or a solo builder shipping Laravel + Inertia.js apps, understanding the OWASP Top 10 and other real-world security flaws is essential.
This guide will walk you through:
- ✅ OWASP Top 10 — with real Laravel + React examples
- ✅ How to fix each one (Gate, Policy, Middleware, Fortify, Sanctum, etc.)
- ✅ Extra vulnerabilities not listed by OWASP (but equally dangerous)
- ✅ A final Laravel Production Security Checklist
📚 Table of Contents
- Secure your Laravel App: OWASP Top 10 + Beyond
- 📚 Table of Contents
- What is OWASP?
- OWASP Top 10 with Laravel Examples
- 1. Broken Access Control (A01)
- 2. Cryptographic Failures (A02)
- 3. Injection (A03)
- 4. Insecure Design (A04)
- 5. Security Misconfiguration (A05)
- 6. Vulnerable Components (A06)
- 7. Identification & Authentication Failures (A07)
- 8. Software and Data Integrity Failures (A08)
- 9. Security Logging & Monitoring Failures (A09)
- 10. Server-Side Request Forgery (SSRF) (A10)
- Beyond OWASP: More Laravel Security Risks
What is OWASP?
The Open Web Application Security Project (OWASP) is a nonprofit foundation that works to improve software security. Their flagship project — the OWASP Top 10 — is a list of the 10 most critical web application security risks.
Laravel gives you great tools to prevent most of these out-of-the-box, but you still need to use them correctly.
OWASP Top 10 with Laravel Examples
1. Broken Access Control (A01)
Problem: Users access data or functions they shouldn’t.
// ❌ Admin route open to all authenticated users
Route::get('/admin/users', [UserController::class, 'index']);
// ✅ Use Policy
$this->authorize('viewAny', User::class);
// ✅ Example Policy
public function viewAny(User $user)
{
return $user->is_admin;
}
// Or wrap with a Gate:
Gate::define('access-admin-panel', fn(User $user) => $user->is_admin);
Tip: For advanced or reusable access logic, consider creating a custom middleware. In Laravel 12, middleware registration has moved from app/Http/Kernel.php to bootstrap/app.php.
How to create and register a custom middleware in Laravel 12:
- Create the middleware:
php artisan make:middleware EnsureUserIsAdmin
- Register the middleware alias in
bootstrap/app.php:
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(...)
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
]);
})
// ...
->create();
- Use the middleware alias in your routes or controllers:
// In routes/web.php
Route::middleware(['admin'])->group(function () {
Route::get('/admin/users', [UserController::class, 'index']);
});
// Or on a single route
Route::get('/admin/dashboard', [DashboardController::class, 'index'])->middleware('admin');
// Or in a controller constructor
public function __construct()
{
$this->middleware('admin');
}
Note: If you are upgrading from Laravel 10 or earlier, remember that middleware registration is no longer done in
app/Http/Kernel.phpbut inbootstrap/app.php.
Tips:
- Always use Policies or Gates for sensitive actions.
- Restrict resource routes with
middleware('can:action,model'). - Never trust client-side checks.
2. Cryptographic Failures (A02)
Problem: Sensitive data like passwords or tokens stored insecurely.
// ❌ Never store raw password
User::create(['password' => $request->password]);
// ✅ Always use hash()
use Illuminate\Support\Facades\Hash;
User::create([
'password' => Hash::make($request->password),
]);
// 🔐 Also use Crypt::encryptString() for sensitive data like tokens or API secrets.
use Illuminate\Support\Facades\Crypt;
$encrypted = Crypt::encryptString($token);
Best practice:
Instead of manually encrypting and decrypting data, you can use Eloquent Attribute Casting. In Laravel 11 and above, use the casts() method instead of the $casts property:
// In your User.php model
protected function casts(): array
{
return [
'api_token' => 'encrypted',
'settings' => 'encrypted:array', // for arrays
];
}
This way, Laravel will automatically encrypt and decrypt the field when saving or retrieving it.
Note: In Laravel 11+, attribute casting is now defined using the
casts()method instead of the$castsproperty. This provides more flexibility and clarity in your models. See the official documentation for more advanced usage.
What should you hash or encrypt?
- User passwords (hash — always with Hash::make())
- API tokens, refresh tokens (encrypt — e.g., with Eloquent cast 'encrypted')
- API keys, integration secrets
- Sensitive user data (e.g., SSNs, tax IDs, addresses, if extra protection is required)
- Configuration data that should not be stored in plain text in the database
Remember:
- Hashing (e.g., for passwords) is one-way — you cannot recover the original value.
- Encryption (e.g., for tokens, secrets) is two-way — you can decrypt the value when needed.
3. Injection (A03)
Problem: User input modifies SQL, Shell, or other commands.
// ❌ Raw SQL vulnerable to injection
DB::select("SELECT * FROM users WHERE email = '{$email}'");
// ✅ Use Query Builder or Eloquent
User::where('email', $email)->first();
Tips:
- Always validate input with Form Requests.
- Use parameterized queries.
- Escape output in Blade with
{{ }}(never use{!! !!}unless sanitized).
4. Insecure Design (A04)
Problem: Security missing at the design level.
Examples:
- Missing rate limiting
- No email verification
- No MFA on account deletion
// ✅ Throttle login
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:5,1');
- Use Laravel Fortify to enable email verification & 2FA easily.
- Design for least privilege and defense in depth.
5. Security Misconfiguration (A05)
Problem: Dangerous settings in production.
APP_DEBUG=trueleaks stack trace.envaccessible if /public not set as web root
✅ Fixes:
APP_DEBUG=falsein .env- Use
php artisan config:cache - Only expose
/publicvia Nginx or Apache - Set proper permissions on storage and .env
6. Vulnerable Components (A06)
Problem: Using outdated or insecure dependencies.
✅ Run audits regularly:
composer audit
npm audit fix
- Pin versions in composer.json and package.json.
- Monitor releases of Laravel, Inertia, Sanctum, etc.
- Remove unused packages.
7. Identification & Authentication Failures (A07)
Problem: Broken authentication logic.
// ❌ Password not hashed
User::create(['password' => $request->password]);
// ✅ Use Fortify for authentication and password reset flows.
- Enforce strong passwords and password policies.
- Use Laravel’s built-in authentication scaffolding.
- Implement account lockout after failed attempts.
8. Software and Data Integrity Failures (A08)
Problem: Tampered files, unsigned assets, broken CI/CD chain.
<!-- ❌ Unverified script -->
<script src="https://cdn.example.com/react.js"></script>
<!-- ✅ Use integrity hash -->
<script src="..." integrity="sha384-..." crossorigin="anonymous"></script>
- Only use trusted, pinned packages.
- Sign deployment artifacts if possible.
- Use Laravel’s signed URLs for sensitive actions.
9. Security Logging & Monitoring Failures (A09)
Problem: You can’t detect or trace suspicious activity.
✅ Log failed logins, password resets, suspicious behaviors:
Log::warning('Login failed', [
'email' => $request->email,
'ip' => $request->ip(),
]);
- Use Laravel Telescope or external tools like Sentry, ELK.
- Set up alerts for suspicious activity.
- Regularly review logs.
10. Server-Side Request Forgery (SSRF) (A10)
Problem: App makes server-side request to untrusted user input.
// ❌ User-controlled URL
Http::get($request->input('url'));
// ✅ Whitelist trusted domains:
$request->validate([
'url' => 'required|url|regex:/^https:\/\/api\.mydomain\.com/',
]);
Http::get($request->input('url'))->throw();
- Never fetch arbitrary URLs from user input.
- Use allow-lists and strict validation.
Beyond OWASP: More Laravel Security Risks
⚠️ 1. Mass Assignment Vulnerabilities
Problem: Attackers can set any model attribute if you don't restrict which fields are mass-assignable.
// ❌ Dangerous: allows all request fields to be set
User::create($request->all());
// ✅ Use $fillable to explicitly allow only certain fields
class User extends Model {
protected $fillable = ['name', 'email', 'password'];
}
// Or use $guarded to block all except listed fields
class User extends Model {
protected $guarded = ['is_admin', 'role'];
}
// ✅ Safer: only pass allowed fields from request
User::create($request->only(['name', 'email', 'password']));
Best Practices:
- Always define
$fillableor$guardedin your models. - Never use
$request->all()for mass assignment. - Validate input with Form Requests.
🔎 2. Cross-Site Scripting (XSS)
Problem: User input is rendered as HTML/JS, allowing attackers to inject scripts.
In Blade:
// ❌ Vulnerable: outputs raw HTML
{!! $userInput !!}
// ✅ Safe: escapes output
{{ $userInput }}
In Inertia/React:
// ❌ Vulnerable: using dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// ✅ Safe: render as plain text, or sanitize first
<div>{userInput}</div>
// or use DOMPurify if you must render HTML
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
Best Practices:
- Never trust user input in HTML.
- Always escape output in Blade (
{{ }}) and sanitize in React if rendering HTML. - Use libraries like DOMPurify for sanitization.
🌐 3. CORS Misconfiguration
Problem: Allowing all origins (*) can expose your API to cross-origin attacks.
Example (config/cors.php):
// ❌ Dangerous
'allowed_origins' => ['*'],
// ✅ Only allow trusted domains
'allowed_origins' => ['https://yourdomain.com', 'https://admin.yourdomain.com'],
Best Practices:
- Never use
*for APIs that require authentication. - Restrict CORS to trusted origins only.
🕓 4. Session Misconfiguration
Problem: Insecure session cookies can be stolen or manipulated.
Example (config/session.php):
// ✅ Secure session settings
'cookie_secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'lax', // or 'strict' for extra security
Best Practices:
- Always use HTTPS in production.
- Set cookies as
secure,http_only, andsame_site. - Rotate session IDs after login.
🗂️ 5. Open Redirects
Problem: Redirecting to URLs from user input can allow phishing attacks.
// ❌ Vulnerable
return redirect($request->input('next'));
// ✅ Only allow internal URLs
$next = $request->input('next');
if ($next && Str::startsWith($next, '/')) {
return redirect($next);
}
return redirect('/dashboard');
Best Practices:
- Never redirect to arbitrary URLs from user input.
- Always validate or sanitize redirect targets.
🗄️ 6. Unsafe File Uploads
Problem: Users can upload dangerous files (e.g., PHP scripts, malware).
// ✅ Validate file type and size
$request->validate([
'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048',
]);
// ✅ Store outside public directory if possible
$path = $request->file('avatar')->store('avatars', 'private');
Best Practices:
- Always validate file type, size, and content.
- Store uploads outside the public directory if possible.
- Never execute or serve uploaded files as code.
🛑 7. .env Exposure
Problem: If your web root is misconfigured, your .env file may be accessible from the web, leaking secrets.
Best Practices:
- Always set your web server root to the
public/directory. - Never commit
.envfiles to version control. - Use environment variables for secrets in production.
Follow me on LinkedIn for more Laravel and DevOps content!
Would you like to learn more about security? Leave a comment below!