Laravel Security: 9 Tips to Prevent Attacks

Do you think you write SECURE code in Laravel? Let's see if you follow these 9 pieces of advice about security in Laravel.


1. Don't use $request->all()

Don't use $request->all() when creating or updating a record.

Controller:

public function store(StoreUserRequest $request) {
User::create($request->all()); // A BIG NO-NO!!!
 
return redirect()->route('dashboard');
}

When using the all() method, it will take all data from the request, even if you have validated it before. So, some people may try to guess your DB fields and send something like is_admin => 1 as a part of the request.

A better approach would be to use $request->only() and provide an array of values.

public function store(StoreUserRequest $request) {
User::create($request->only('name', 'email', 'password'));
 
return redirect()->route('dashboard');
}

Or, if you are using Form Requests for validation, even better way is to use $request->validated() which will take ONLY the keys that were inside of the rules() method of the Form Request.

public function store(StoreUserRequest $request) {
User::create($request->validated());
 
return redirect()->route('dashboard');
}

We have a more in-depth tutorial about $request->all() here.


2. Raw SQL Queries Parameter Binding

When making a query using the raw syntax, be careful how you inject values. For example, you have a products list and would query by a min price:

$minPrice = $request->input('min_price')
$products = Product::whereRaw("price > $minPrice")->get();

If user wouldn't alter the query param everything would work. But, in this case, if user would pass $minPrice = "0 OR '1'='1'", they would get ALL the products.

A proper way would be to use parameter binding.

$minPrice = $request->input('min_price');
$products = Product::whereRaw("price > ?", [$minPrice])->get();

This way when the same query would be tried to execute an error would appear instead: Illegal string offset "0 OR '1'='1'".

Another example of such non-sanitized SQL parameter is a classical xkcd comic:

And, of course, add the parameter validation beforehand.


3. Don't Leave APP_DEBUG=true In Production

Leaving APP_DEBUG set as true can end with serious consequences. If you do that, in case of error the user receives the full information instead of a general message like "Server Error" or "Service Unavailable".

So, they could see sensitive information through detailed error messages, including stack traces, environment variables, database credentials, etc. This information could give attacker a lot of information for further exploitation.

In other words, APP_DEBUG=true will allow users to see the same error information as you see locally.


4. Don't Forget To Set APP_ENV=production In Production

When APP_ENV is set to production, Laravel adds some extra security checks which you may enable. Besides some optimizations, it disables debugging tools and improves security.

It can also prevent from accidentally running commands like php artisan migrate:fresh while connected to the production server.

Since Laravel v11.9, there is a Prohibitable trait which when is added to your custom Artisan commands won't run command in the production environment.

You can also limit commands by running in a certain environment by calling prohibitDestructiveCommands() method on the command in the Service Provider and providing a boolean value.

app/Providers/AppServiceProvider.php:

use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Console\Migrations\FreshCommand;
 
class AppServiceProvider extends ServiceProvider
{
// ...
 
public function boot(): void
{
DB::prohibitDestructiveCommands($this->app->isProduction());
}
}

You can watch a YouTube video about this prohibit feature here.


5. Don't Leave .env File in Public

Don't leave .env publicly anywhere. This file contains sensitive information like database credentials. It should be always added to the .gitignore and never pushed to the GitHub. Especially if repository is public, then everyone can find it. You can push only .env.example file with default example/empty values.

Another essential thing: prevent public access to your .env file on the web-server level! This means yourdomain.com/.env shouldn't be accessible. Usually, this happens when the server configuration is bad or when using shared hosting. The only accessible folder from the web should be /public.


6. Triple-Test Multi-tenancy

When working with single-database multi-tenancy, be extra careful with the where() queries to allow users to access data only they should access.

One way is to apply global scopes for the model.

protected static function booted()
{
static::addGlobalScope('created_by_user_id', function (Builder $builder) {
$builder->where('user_id', auth()->id());
});
}

My suggestion would be to write automated tests for as many cases as possible. The most basic test is to check that the user can see only their data and not others.

use App\Models\Task;
use App\Models\User;
 
test('user can see only their tasks', function () {
$user = User::factory()->create();
$otherUser = User::factory()->create();
 
$otherUserTask = Task::factory()->create([
'user_id' => $otherUser->id,
'title' => 'Other users task'
]);
 
$userTask = Task::factory()->create([
'user_id' => $user->id,
'title' => 'My task'
]);
 
$response = $this->actingAs($user)->get('/tasks');
 
$response->assertOk()
->assertSeeText('My task')
->assertDontSeeText('Other users task');
});

If you haven't started with testing, we have a course Testing in Laravel For Beginners.

Also, we have a course about multi-tenancy: Laravel Multi-Tenancy: All You Need To Know.


7. You MAY Return 404 instead of 403

This is an extra optional measure to hide some information from a potential hacker. There may be pages, where user shouldn't even know that they exist.

Usually, when you check for some permissions with Gate/Policy, if user doesn't have it a "403 Forbidden" response is thrown.

When doing a check using abort_if() instead of a 403 status code you can provide a 404.

abort_if(! Auth::user()->isAdmin(), 404);

When using Policies you can return a different response based on the condition.

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
 
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::denyWithStatus(404);
}

Instead of denyWithStatus(404), Laravel also has a denyAsNotFound() method.

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
 
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::denyAsNotFound();
}

Also, you can set the response code globally in the Service Provider.

app/Providers/AppServiceProvider.php:

use Illuminate\Support\Facades\Gate;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\ServiceProvider;
 
class AppServiceProvider extends ServiceProvider
{
// ...
 
public function boot(): void
{
Gate::defaultDenialResponse(Response::denyAsNotFound());
}
}

8. XSS: Be Careful When Outputting Unescaped Data

When using Blade you can use {!! !!} to show unescaped data. But be very careful when displaying raw HTML code if it comes from the users. You don't know what users can try to do, what malicious JavaScript code they would try to run.

When an attacker leaves a JS code in the textarea, such as alert() function, others will see that alert message executed.

But this is just an example: instead of a simple alert, imagine what else the attackers can do, like injecting the code to steal sensitive information.

It is better to sanitize data before outputting it. You can use function like htmlspecialchars() or strip_tags(). Or package like Purifier.

Or, even better, if possible, do NOT allow users to enter any data that would be outputted with {!! !!}.


9. Update Laravel/PHP/Packages Version

The final point. Frequently update versions of Laravel, PHP, and packages you use. Some versions can have basic security updates but others may fix critical security issues.

For example, with Livewire v3.4.9 release, an XSS security vulnerability has been patched. You can check live demonstration of this vulnerability during Laracon AU by Stephen Rees-Carter on YouTube here.

Another example, in Filament v3.2.15 an XSS vulnerability was patched.

While Laravel itself didn't have many security vulnerabilities, the last one was reported in November 2024 and was marked as high severity.

PHP versions also receives security updates for a limited time. At the time of writing, PHP 8.0 doesn't release any security updates anymore, and PHP 8.1 will stop releasing them in January 2026. PHP itself gets vulnerability reports quite often, and quite a few have a high severity level.

So, update regularly, and often check if currently used versions don't have vulnerabilities.


So, any of these tips were new to you? Anything you need to run and double-check in your Laravel projects and server configurations?

avatar

Nice article with a lot of good tips that are easy to overlook!

One tip I'd suggest is that when handling files uploaded through the request, don't use getClientOriginalName() or getClientOriginalExtension() when storing on the server, especially in a publicly-accessible directory.

The safer alternative is to simply use the hashName() (generates a random filename) and extension() (based on MIME-type, in line with Laravel's validation) methods.

Here's these methods in the Laravel docs. Stephen Rees-Carter also has a great write-up about this too.

👍 8
avatar
You can use Markdown
avatar
You can use Markdown

Recent New Courses