Security 16 min read

OWASP Top 10 for Laravel: A Practical Guide

A hands-on mapping of every OWASP Top 10 (2021) category to specific Laravel vulnerabilities, with code examples of what goes wrong and how to fix it.

Matt King
Matt King
March 9, 2026
Last updated: March 9, 2026
OWASP Top 10 for Laravel: A Practical Guide

The OWASP Top 10 is the most widely referenced standard for web application security risks. The 2021 edition reflects data from hundreds of organizations and thousands of applications.

This guide maps each OWASP Top 10 category to specific Laravel vulnerabilities. For every item, you will find: what the vulnerability is, how it applies to Laravel, a code example of the problem, a code example of the fix, and how to test for it.


A01: Broken Access Control

Broken access control means users can act outside their intended permissions. They can view other users' data, modify records they should not have access to, or escalate their privileges.

This moved to the number one spot in 2021 because it is the most commonly found vulnerability in real-world applications.

How It Applies to Laravel

Laravel provides middleware, gates, and policies for access control. The vulnerability appears when developers skip these protections or implement them incorrectly.

The Vulnerability

// Insecure: Any authenticated user can view any order
Route::get('/orders/{id}', function ($id) {
    $order = Order::findOrFail($id);
    return view('orders.show', compact('order'));
})->middleware('auth');

An authenticated user can change the {id} parameter to view any other user's order. This is called an Insecure Direct Object Reference (IDOR).

The Fix

// Secure: Users can only view their own orders
Route::get('/orders/{order}', function (Order $order) {
    $this->authorize('view', $order);
    return view('orders.show', compact('order'));
})->middleware('auth');

// app/Policies/OrderPolicy.php
public function view(User $user, Order $order): bool
{
    return $user->id === $order->user_id;
}

Or scope the query to the authenticated user:

Route::get('/orders/{order}', function (Order $order) {
    // Route model binding scoped to the user
    $order = auth()->user()->orders()->findOrFail($order->id);
    return view('orders.show', compact('order'));
})->middleware('auth');

How to Test

  • Try accessing resources with IDs belonging to other users
  • Test API endpoints with different user tokens
  • Verify that admin-only routes return 403 for regular users
  • Use Laravel's built-in policy testing: $this->assertCannot('view', $otherUsersOrder)

A02: Cryptographic Failures

Previously called "Sensitive Data Exposure," this category covers failures to properly protect data at rest and in transit. This includes weak encryption, missing HTTPS, exposed credentials, and plain-text storage of sensitive data.

How It Applies to Laravel

Laravel uses the APP_KEY for encryption and provides the encrypt() and decrypt() helpers. Problems arise when sensitive data is stored in plain text, when HTTPS is not enforced, or when the APP_KEY is weak or exposed.

The Vulnerability

// Insecure: Storing sensitive data in plain text
$user = User::create([
    'name' => $request->name,
    'email' => $request->email,
    'ssn' => $request->ssn, // Plain text SSN in the database
    'password' => $request->password, // Plain text password!
]);

The Fix

// Secure: Encrypt sensitive fields, hash passwords
$user = User::create([
    'name' => $request->name,
    'email' => $request->email,
    'ssn' => encrypt($request->ssn), // Encrypted at rest
    'password' => Hash::make($request->password), // Bcrypt hash
]);

// In the User model, use encrypted casting
protected function casts(): array
{
    return [
        'ssn' => 'encrypted',
        'password' => 'hashed',
    ];
}

Force HTTPS in production:

// app/Providers/AppServiceProvider.php
public function boot()
{
    if (app()->environment('production')) {
        URL::forceScheme('https');
    }
}

How to Test

  • Check your database directly for plain-text sensitive fields
  • Verify HTTPS is enforced by accessing http:// and confirming redirect to https://
  • Run php artisan config:show app.key to ensure the APP_KEY is set
  • Check that the HSTS header is present in responses

A03: Injection

Injection flaws occur when untrusted data is sent to an interpreter as part of a command or query. SQL injection is the most well-known form, but it also includes OS command injection, LDAP injection, and more.

How It Applies to Laravel

Laravel's Eloquent ORM uses parameterized queries by default, which prevents SQL injection. But developers can bypass this protection with raw queries, DB::raw(), or string concatenation.

The Vulnerability

// Insecure: Raw SQL with string concatenation
$users = DB::select("SELECT * FROM users WHERE email = '" . $request->email . "'");

// Insecure: DB::raw with unescaped input
$users = User::whereRaw("email = '" . $request->input('email') . "'")->get();

// Insecure: orderBy with unsanitized input
$users = User::orderBy($request->input('sort_by'))->get();

An attacker can inject SQL through the email parameter:

email=' OR '1'='1'; DROP TABLE users; --

The Fix

// Secure: Parameterized raw query
$users = DB::select("SELECT * FROM users WHERE email = ?", [$request->email]);

// Secure: Eloquent (parameterized by default)
$users = User::where('email', $request->email)->get();

// Secure: DB::raw with bindings
$users = User::whereRaw("email = ?", [$request->input('email')])->get();

// Secure: Whitelist allowed sort columns
$sortable = ['name', 'email', 'created_at'];
$sortBy = in_array($request->input('sort_by'), $sortable)
    ? $request->input('sort_by')
    : 'created_at';
$users = User::orderBy($sortBy)->get();

How to Test

  • Search your codebase for DB::raw, whereRaw, selectRaw, and string concatenation in queries
  • Use static analysis tools (PHPStan, Psalm) to detect unsafe query patterns
  • Test input fields with SQL injection payloads using tools like sqlmap or OWASP ZAP

A04: Insecure Design

Insecure design is a broad category that focuses on flaws in the application's design and architecture, not just implementation bugs. This includes missing rate limiting, lack of input validation, and absence of business logic controls.

How It Applies to Laravel

Common insecure design patterns in Laravel applications include unlimited password reset attempts, unthrottled API endpoints, and missing business logic validation.

The Vulnerability

// Insecure: No rate limiting on password reset
Route::post('/forgot-password', [PasswordResetController::class, 'send']);

// Insecure: No business logic validation on coupon usage
public function applyCoupon(Request $request)
{
    $coupon = Coupon::where('code', $request->code)->firstOrFail();
    // No check if the coupon has already been used by this user
    // No check if the coupon has reached its usage limit
    $request->user()->cart->applyCoupon($coupon);
}

The Fix

// Secure: Rate-limited password reset
Route::post('/forgot-password', [PasswordResetController::class, 'send'])
    ->middleware('throttle:5,1'); // 5 attempts per minute

// Secure: Business logic validation
public function applyCoupon(Request $request)
{
    $coupon = Coupon::where('code', $request->code)->firstOrFail();

    if ($coupon->hasBeenUsedBy($request->user())) {
        return back()->withErrors(['code' => 'You have already used this coupon.']);
    }

    if ($coupon->hasReachedUsageLimit()) {
        return back()->withErrors(['code' => 'This coupon is no longer available.']);
    }

    if ($coupon->isExpired()) {
        return back()->withErrors(['code' => 'This coupon has expired.']);
    }

    $request->user()->cart->applyCoupon($coupon);
}

How to Test

  • Review your routes for endpoints that lack rate limiting
  • Test business-critical flows (payments, coupons, invites) for logic bypasses
  • Use threat modeling to identify design-level risks before writing code

A05: Security Misconfiguration

This is the most common vulnerability in Laravel applications. It includes debug mode in production, default credentials, exposed development tools, missing security headers, and overly permissive CORS policies.

How It Applies to Laravel

Laravel ships with powerful development tools that become security liabilities if left accessible in production: Telescope, Horizon, Ignition, and Debugbar.

The Vulnerability

# .env in production - WRONG
APP_DEBUG=true
APP_ENV=local

# config/cors.php - WRONG
'allowed_origins' => ['*'],
// Telescope accessible without authentication
// Horizon accessible without authentication
// No security headers set

The Fix

# .env in production - CORRECT
APP_DEBUG=false
APP_ENV=production
// config/cors.php - CORRECT
'allowed_origins' => ['https://yourfrontend.com'],

// app/Http/Middleware/SecurityHeaders.php
public function handle($request, Closure $next)
{
    $response = $next($request);

    $response->headers->set('X-Frame-Options', 'SAMEORIGIN');
    $response->headers->set('X-Content-Type-Options', 'nosniff');
    $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
    $response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
    $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');

    return $response;
}

// Telescope: restrict access in TelescopeServiceProvider
protected function gate()
{
    Gate::define('viewTelescope', function ($user) {
        return in_array($user->email, ['admin@yourapp.com']);
    });
}

How to Test

  • Visit your production URL with /telescope, /horizon, /_ignition
  • Check for debug mode by triggering a 404
  • Inspect response headers for missing security headers
  • Use StackShield to check all of these automatically

A06: Vulnerable and Outdated Components

Using components (libraries, frameworks, packages) with known vulnerabilities is a direct path to exploitation. Attackers actively scan for applications running outdated software with published CVEs.

How It Applies to Laravel

Laravel applications rely heavily on Composer packages. A single outdated dependency can introduce known vulnerabilities.

The Vulnerability

// composer.json with outdated, vulnerable packages
{
    "require": {
        "laravel/framework": "8.0",
        "facade/ignition": "2.3.6",
        "guzzlehttp/guzzle": "6.5.0"
    }
}

Ignition 2.3.6 has a known RCE vulnerability (CVE-2021-3129). Guzzle 6.5.0 has SSRF vulnerabilities.

The Fix

Run composer audit regularly:

composer audit

Update packages:

composer update --with-all-dependencies

Add to your CI/CD pipeline:

- name: Check for vulnerable dependencies
  run: composer audit --format=json
  # This command exits with code 1 if vulnerabilities are found

How to Test

  • Run composer audit locally and in CI
  • Use GitHub Dependabot or similar tools for automatic alerts
  • Check the Laravel security releases page regularly
  • Monitor your deployed application's Laravel version externally

A07: Identification and Authentication Failures

This covers weak authentication mechanisms: broken login flows, session fixation, weak passwords, missing multi-factor authentication, and credential stuffing vulnerabilities.

How It Applies to Laravel

Laravel provides solid authentication scaffolding through Breeze, Jetstream, and Fortify. Problems occur when developers implement custom authentication or misconfigure the defaults.

The Vulnerability

// Insecure: Custom login without rate limiting or lockout
public function login(Request $request)
{
    $credentials = $request->only('email', 'password');

    if (Auth::attempt($credentials)) {
        return redirect('/dashboard');
    }

    return back()->withErrors(['email' => 'Invalid credentials']);
}

// Insecure: Session not regenerated after login
public function login(Request $request)
{
    if (Auth::attempt($request->only('email', 'password'))) {
        // Missing: $request->session()->regenerate();
        return redirect('/dashboard');
    }
}

The Fix

// Secure: Login with rate limiting and session regeneration
public function login(Request $request)
{
    $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    // Rate limiting
    $throttleKey = Str::lower($request->email) . '|' . $request->ip();

    if (RateLimiter::tooManyAttempts($throttleKey, 5)) {
        $seconds = RateLimiter::availableIn($throttleKey);
        throw ValidationException::withMessages([
            'email' => "Too many login attempts. Please try again in {$seconds} seconds.",
        ]);
    }

    if (!Auth::attempt($request->only('email', 'password'))) {
        RateLimiter::hit($throttleKey);
        return back()->withErrors(['email' => 'Invalid credentials']);
    }

    RateLimiter::clear($throttleKey);
    $request->session()->regenerate();

    return redirect('/dashboard');
}

Use strong password rules:

// In your registration validation
'password' => ['required', 'confirmed', Password::min(8)
    ->mixedCase()
    ->numbers()
    ->symbols()
    ->uncompromised()], // Checks against known breached passwords

How to Test

  • Attempt rapid-fire login requests to verify rate limiting works
  • Check that session IDs change after login
  • Verify password requirements enforce minimum strength
  • Test that failed login responses do not reveal whether the email exists

A08: Software and Data Integrity Failures

This category covers code and infrastructure that does not protect against integrity violations. This includes insecure deserialization, unverified updates, and tampered CI/CD pipelines.

How It Applies to Laravel

Laravel uses serialization in queued jobs, cached data, and session storage. Insecure deserialization can lead to remote code execution.

The Vulnerability

// Insecure: Deserializing user input
$data = unserialize($request->input('data'));

// Insecure: No integrity check on webhook payloads
Route::post('/webhooks/payment', function (Request $request) {
    $payload = $request->all();
    // Processing webhook without verifying signature
    processPayment($payload);
});

The Fix

// Secure: Use JSON instead of serialization for user input
$data = json_decode($request->input('data'), true);

// Secure: Verify webhook signatures
Route::post('/webhooks/stripe', function (Request $request) {
    $payload = $request->getContent();
    $signature = $request->header('Stripe-Signature');

    try {
        $event = \Stripe\Webhook::constructEvent(
            $payload,
            $signature,
            config('services.stripe.webhook_secret')
        );
    } catch (\Exception $e) {
        return response('Invalid signature', 400);
    }

    processStripeEvent($event);
});

Lock Composer dependencies:

# Always commit composer.lock
# Use exact versions in production
composer install --no-dev --prefer-dist

How to Test

  • Search your codebase for unserialize() with user-controlled input
  • Verify that all webhook endpoints validate signatures
  • Ensure composer.lock is committed and used in deployments
  • Verify that your CI/CD pipeline uses pinned action versions

A09: Security Logging and Monitoring Failures

Without proper logging and monitoring, you cannot detect attacks, respond to breaches, or perform forensic analysis. This category was previously called "Insufficient Logging & Monitoring."

How It Applies to Laravel

Laravel provides logging through Monolog, but many applications only log errors, not security-relevant events. Login attempts, permission changes, and data exports often go unrecorded.

The Vulnerability

// Insecure: No logging of security events
public function login(Request $request)
{
    if (Auth::attempt($request->only('email', 'password'))) {
        return redirect('/dashboard');
    }

    // Failed login not logged - brute force goes undetected
    return back()->withErrors(['email' => 'Invalid credentials']);
}

// Insecure: Admin actions not logged
public function deleteUser(User $user)
{
    $user->delete();
    return redirect('/admin/users');
}

The Fix

// Secure: Log security-relevant events
public function login(Request $request)
{
    if (Auth::attempt($request->only('email', 'password'))) {
        Log::info('Successful login', [
            'user_id' => Auth::id(),
            'ip' => $request->ip(),
            'user_agent' => $request->userAgent(),
        ]);

        $request->session()->regenerate();
        return redirect('/dashboard');
    }

    Log::warning('Failed login attempt', [
        'email' => $request->email,
        'ip' => $request->ip(),
        'user_agent' => $request->userAgent(),
    ]);

    return back()->withErrors(['email' => 'Invalid credentials']);
}

// Secure: Audit trail for admin actions
public function deleteUser(User $user)
{
    Log::warning('User deleted by admin', [
        'deleted_user_id' => $user->id,
        'deleted_user_email' => $user->email,
        'admin_id' => auth()->id(),
        'ip' => request()->ip(),
    ]);

    $user->delete();
    return redirect('/admin/users');
}

Configure log alerting:

// config/logging.php - Send critical logs to Slack
'channels' => [
    'slack' => [
        'driver' => 'slack',
        'url' => env('LOG_SLACK_WEBHOOK_URL'),
        'level' => 'warning',
    ],
    'stack' => [
        'driver' => 'stack',
        'channels' => ['daily', 'slack'],
    ],
],

How to Test

  • Trigger a failed login and check if it appears in your logs
  • Perform an admin action and verify the audit trail
  • Check that your logging configuration sends alerts for warning-level events
  • Verify that logs do not contain sensitive data (passwords, tokens, full credit card numbers)

A10: Server-Side Request Forgery (SSRF)

SSRF occurs when an application fetches a remote resource based on user-supplied input without validating the destination. Attackers use this to access internal services, cloud metadata endpoints, and other resources behind the firewall.

How It Applies to Laravel

Laravel applications that fetch URLs provided by users (webhook URLs, avatar URLs, import URLs) are vulnerable to SSRF if the destination is not validated.

The Vulnerability

// Insecure: Fetching a user-supplied URL without validation
public function fetchPreview(Request $request)
{
    $url = $request->input('url');
    $response = Http::get($url); // User can point this to internal services

    return response()->json([
        'title' => $this->parseTitle($response->body()),
    ]);
}

An attacker can use this to access:

# AWS metadata endpoint (steal IAM credentials)
http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Internal services
http://localhost:6379/ (Redis)
http://localhost:3306/ (MySQL)
http://internal-admin.company.local/

The Fix

// Secure: Validate and restrict URLs
public function fetchPreview(Request $request)
{
    $request->validate([
        'url' => ['required', 'url', 'regex:/^https:\/\//'],
    ]);

    $url = $request->input('url');

    // Block internal IPs
    $host = parse_url($url, PHP_URL_HOST);
    $ip = gethostbyname($host);

    if ($this->isInternalIp($ip)) {
        return response()->json(['error' => 'Invalid URL'], 422);
    }

    $response = Http::timeout(5)->get($url);

    return response()->json([
        'title' => $this->parseTitle($response->body()),
    ]);
}

private function isInternalIp(string $ip): bool
{
    return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
}

How to Test

  • Test URL input fields with internal addresses (127.0.0.1, 169.254.169.254, 10.0.0.1)
  • Use OWASP ZAP to test for SSRF patterns
  • Check that the application rejects non-HTTPS URLs when appropriate
  • Verify that DNS rebinding attacks are mitigated

Summary

Here is a quick reference of each OWASP Top 10 category and the primary Laravel defense:

# Category Primary Laravel Defense
A01 Broken Access Control Policies, Gates, scoped queries
A02 Cryptographic Failures encrypt(), Hash::make(), force HTTPS
A03 Injection Eloquent ORM, parameterized queries
A04 Insecure Design Rate limiting, business logic validation
A05 Security Misconfiguration APP_DEBUG=false, security headers, restrict dev tools
A06 Vulnerable Components composer audit, dependency updates
A07 Auth Failures Breeze/Fortify, rate limiting, session regeneration
A08 Integrity Failures JSON over serialization, webhook signature verification
A09 Logging Failures Monolog, security event logging, alerting
A10 SSRF URL validation, IP blocking, HTTPS enforcement

The OWASP Top 10 is not a checklist you run once. It is a framework for thinking about security throughout your development lifecycle. Laravel gives you strong defaults for many of these categories, but defaults only protect you when they are configured correctly and used consistently.

For automated, continuous checking of categories A01 through A10 from the outside (the way an attacker sees your application), StackShield runs 30+ security checks against your Laravel application and alerts you when something changes.

Start a free 14-day trial of StackShield to see how your application scores against the OWASP Top 10.

Frequently Asked Questions

Does Laravel protect against the OWASP Top 10 out of the box?

Laravel provides built-in protection against several OWASP Top 10 categories, including SQL injection (via Eloquent ORM), CSRF (via middleware), and XSS (via Blade escaping). However, these protections only work when used correctly. Raw queries, {!! !!} Blade syntax, misconfigured middleware, and disabled default protections can reintroduce vulnerabilities. Laravel gives you the tools, but you still need to use them properly.

Which OWASP Top 10 vulnerability is most common in Laravel applications?

Security Misconfiguration (A05) is the most common issue in Laravel applications. This includes debug mode enabled in production, exposed .env files, accessible development tools (Telescope, Horizon, Ignition), missing security headers, and permissive CORS policies. These are configuration issues, not code issues, which makes them easy to introduce and easy to miss in code review.

How do I test my Laravel application against the OWASP Top 10?

Use a combination of approaches: static analysis tools (PHPStan, Psalm) catch code-level issues like SQL injection and type safety problems. Dynamic analysis tools (OWASP ZAP, Burp Suite) test your running application for injection, authentication, and access control flaws. Dependency scanning (composer audit) catches known vulnerabilities in packages. External monitoring tools like StackShield continuously check for security misconfiguration, missing headers, and exposed endpoints.

What is the difference between OWASP Top 10 2017 and 2021?

The 2021 update reorganized and renamed several categories. Broken Access Control moved to the number one position. Three new categories were added: Insecure Design (A04), Software and Data Integrity Failures (A08), and Server-Side Request Forgery (A10). XML External Entities (XXE) was merged into Security Misconfiguration. The 2021 version focuses more on root causes rather than symptoms.

How often is the OWASP Top 10 updated?

The OWASP Top 10 is updated every 3 to 4 years based on data collected from hundreds of organizations. The most recent version is from 2021. The list reflects real-world vulnerability data, not theoretical risks, so it remains the most widely referenced standard for web application security priorities.

Related Security Terms