Laravel 11 Security Audit Guide (Part 1 of 3)
Fongoh Martin T.

Fongoh Martin T. @bosz

About: Hey fam :) I love product development, business and startup. As a dev, I code in Laravel, React & React Native. Be glad to connect with you, hit me up lets connect :)

Location:
Silicon Mountain, Buea, Cameroon
Joined:
Nov 21, 2019

Laravel 11 Security Audit Guide (Part 1 of 3)

Publish Date: Jun 14
0 0

Laravel 11 Security Audit Guide (Part 1 of 3)

As developers and indie hackers, we often focus on shipping features fast, but security is usually an afterthought. In this 3-part series, I’ll walk you through a hands-on Laravel 11 security audit we did on a real-world app, exposing common vulnerabilities and how to fix them.

Each part will tackle four key security areas. Let’s dive into Part 1.

1. Brute Force Attacks on Login

The Problem

Brute force attacks are one of the most common and basic types of attack on any web application. These attacks happen when an attacker writes a script that tries many combinations of usernames and passwords until they find one that works. Laravel's login route (/login or /api/login) is a prime target if not properly protected. Without any rate limiting, an attacker could make thousands of login attempts per minute, eventually gaining unauthorized access to user accounts.

This isn’t just theoretical—many bots continuously scan the web looking for login forms to exploit. Even a simple email/password form can be an entry point if brute force protection is not in place.

How to Test

Use tools like Burp Suite, Hydra, or even a custom script with curl to simulate a brute force attempt.

  • Start with a list of common email addresses or usernames.
  • Use a password dictionary (like rockyou.txt) and send multiple POST requests to the login route.
  • Monitor the response for a successful login or for error messages that suggest rate limiting is not in place.

Example Payload (via Burp):

POST /login
Content-Type: application/x-www-form-urlencoded

email=test@example.com&password=123456
Enter fullscreen mode Exit fullscreen mode

Send 10,000 of these with different passwords in a loop. If the application doesn't throttle or block them, it's vulnerable.

How to Fix It

Laravel provides built-in rate limiting using middleware. You can add the throttle middleware to limit how many attempts a user can make in a given timeframe.

Route::middleware(['throttle:5,1'])->group(function () {
    Route::post('/login', [LoginController::class, 'login']);
});
Enter fullscreen mode Exit fullscreen mode

This configuration means a user can only make 5 login attempts per minute. Any more and they will receive a 429 Too Many Requests response.

For APIs, you can use Laravel's built-in api throttle via RouteServiceProvider or define custom rate limits in Route::middleware().

Additional Advice

  • Use Laravel's Lockout feature in LoginController.
  • Implement CAPTCHA after a few failed attempts.
  • Send alert emails on multiple failed logins from the same IP.
  • Log suspicious IPs and temporarily block repeat offenders.

2. Insecure Direct Object Reference (IDOR)

The Problem

IDOR vulnerabilities happen when a user can access data or functionality that they shouldn’t be able to, simply by changing the value of an identifier (like an ID in the URL). For instance, suppose you have an endpoint like /orders/123. If there’s no check in place, a user could change it to /orders/124 and view someone else’s order.

This happens when developers rely only on route model binding or Eloquent queries without checking whether the authenticated user actually owns the resource.

How to Test

  • Login as a regular user (say, User A).
  • Perform a legitimate request to /orders/123.
  • Change the ID in the URL to /orders/124, /orders/125, and so on.
  • If you’re able to view or manipulate orders that belong to another user, that’s an IDOR.

The Fix

Always verify that the current user owns the resource before showing or modifying it.

public function show($id)
{
    $order = Order::findOrFail($id);

    if ($order->user_id !== auth()->id()) {
        abort(403, 'Unauthorized access.');
    }

    return view('orders.show', compact('order'));
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, use Laravel's authorization system with policies:

php artisan make:policy OrderPolicy --model=Order
Enter fullscreen mode Exit fullscreen mode

In OrderPolicy:

public function view(User $user, Order $order)
{
    return $user->id === $order->user_id;
}
Enter fullscreen mode Exit fullscreen mode

Then in controller:

$this->authorize('view', $order);
Enter fullscreen mode Exit fullscreen mode

Additional Advice

  • Use UUIDs instead of incremental IDs.
  • Obfuscate IDs on the frontend if needed.
  • Never trust that because a user is authenticated, they can access any model instance.

3. SQL Injection

The Problem

SQL Injection is a serious and potentially catastrophic vulnerability where an attacker can inject raw SQL commands through user input to manipulate your database. This is particularly dangerous when developers build queries using string interpolation instead of prepared statements.

Example of bad code:

$users = DB::select("SELECT * FROM users WHERE email = '$email'");
Enter fullscreen mode Exit fullscreen mode

If someone enters test@example.com' OR 1=1 --, the query becomes:

SELECT * FROM users WHERE email = 'test@example.com' OR 1=1 --'
Enter fullscreen mode Exit fullscreen mode

This would return all users in the database.

How to Test

  • Identify endpoints that include user input in SQL queries.
  • Use payloads like ' OR 1=1 -- or ' UNION SELECT null, version(), null -- to see if you can break the query.
  • Monitor database behavior or verbose error messages.

The Fix

Use Laravel’s query builder or Eloquent ORM, which uses parameter binding to prevent injection.

Safe examples:

$users = DB::select("SELECT * FROM users WHERE email = ?", [$email]);
Enter fullscreen mode Exit fullscreen mode

Even better:

$user = User::where('email', $email)->first();
Enter fullscreen mode Exit fullscreen mode

Laravel’s query builder escapes and binds variables by default.

Additional Advice

  • Never disable SQL query escaping.
  • Avoid raw SQL unless absolutely necessary, and if used, always bind parameters.
  • Use Laravel’s DB::raw() carefully and sparingly.

4. Cross-Site Scripting (XSS)

The Problem

XSS attacks occur when an attacker is able to inject malicious scripts (usually JavaScript) into pages that other users will load. These scripts can steal cookies, session tokens, or even redirect users to phishing sites.

For instance, if a user submits the following input into a comment box:

<script>alert('XSS')</script>
Enter fullscreen mode Exit fullscreen mode

And your view simply renders that like:

<div>{!! $comment->body !!}</div>
Enter fullscreen mode Exit fullscreen mode

It will execute in every other user’s browser that views the page.

How to Test

  • Submit HTML and JavaScript tags in text input fields.
  • Use payloads like <script>alert(1)</script> or <img src=x onerror=alert(1)>.
  • Observe if the script executes when the input is displayed.

How to Fix It

  • Always use {{ $value }} in Blade templates, not {!! !!} unless you are 100% sure the content is sanitized.
  • Use Laravel’s e() helper:
<div>{{ e($comment->body) }}</div>
Enter fullscreen mode Exit fullscreen mode
  • Escape user input at output, not input.
  • Use libraries like HTMLPurifier if you must allow some HTML.

Additional Advice

  • Apply a strong Content Security Policy (CSP).
  • Enable browser protections by setting HTTP headers like X-XSS-Protection and Content-Security-Policy.
  • Avoid storing or rendering user-supplied HTML if possible.

Here is Part 2, where we’ll dive into CSRF protection, file upload vulnerabilities, token hijacking, and API rate limiting.

If you’ve experienced any of these security flaws in your Laravel apps, share your thoughts or tips in the comments!

Comments 0 total

    Add comment