Secure your Laravel App: OWASP Top 10 + Beyond

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


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:

  1. Create the middleware:
php artisan make:middleware EnsureUserIsAdmin
  1. 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();
  1. 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.php but in bootstrap/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 $casts property. 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=true leaks stack trace
  • .env accessible if /public not set as web root

✅ Fixes:

  • APP_DEBUG=false in .env
  • Use php artisan config:cache
  • Only expose /public via 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 $fillable or $guarded in 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, and same_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 .env files 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!

Comments (0)
Leave a comment

© 2026 All rights reserved.