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
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']);
});
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 inLoginController
. - 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'));
}
Alternatively, use Laravel's authorization system with policies:
php artisan make:policy OrderPolicy --model=Order
In OrderPolicy
:
public function view(User $user, Order $order)
{
return $user->id === $order->user_id;
}
Then in controller:
$this->authorize('view', $order);
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'");
If someone enters test@example.com' OR 1=1 --
, the query becomes:
SELECT * FROM users WHERE email = 'test@example.com' OR 1=1 --'
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]);
Even better:
$user = User::where('email', $email)->first();
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>
And your view simply renders that like:
<div>{!! $comment->body !!}</div>
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>
- 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
andContent-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!