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.
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 tohttps:// - Run
php artisan config:show app.keyto 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 auditlocally 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.lockis 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
Related Articles
Laravel Debug Mode in Production: Why It's Dangerous and How to Fix It
Debug mode in production exposes stack traces, database credentials, environment variables, and internal paths. Learn exactly what it reveals, how attackers use it, and how to make sure it never reaches production.
SecurityIs Your Laravel .env File Exposed? How to Check and Fix It
Your .env file contains database credentials, API keys, and encryption secrets. If it's accessible from the web, attackers already have everything they need. Here's how to check and fix it.
SecurityLaravel Telescope in Production: Security Risks You Need to Know
Laravel Telescope records every request, query, job, and log entry in your application. Left exposed in production, it gives attackers a real-time view into your entire system.