Laravel

Security

SQL injection

Laravel provides a robust Query Builder and Eloquent ORM. And thanks to them most of the queries are protected in Laravel applications by default, so for example a query like

Product::query()->where('category_id', $request->input('categoryId'))->get();

will be automatically protected, since under the hood Laravel will translate the code into a prepared statement and execute.

But developers usually make mistakes by assuming Laravel protects from all SQL injections, while there are some attack vectors Laravel can’t protect against. Here are the most common causes of SQL injections.

Column name

SQL Injection via column name. It’s not safe to pass user-controlled column names to the query builder. Here is a warning from Laravel’s documentation.

PDO does not support binding column names. Therefore, you should never allow user input to dictate the column names referenced by your queries, including "order by" columns.

Reference: Laravel Documentation

So the following code will be vulnerable to an SQL injection:

$categoryId = $request->input('categoryId');
$orderBy = $request->input('orderBy');
Product::query()
    ->where('category_id', $categoryId)
    ->orderBy($orderBy)
    ->get();

This way, someone can use a query like http://example.com/users?orderBy=id->test"' ASC, IF((SELECT count (*) FROM users ) < 10, SLEEP(20), SLEEP(0)) DESC -- "'

In short: do not pass user-controlled column names to the Query Builder without allow-listing them.

Validation rules

SQL Injection via validation rules. Let’s look at the following simplified validation code

$userId = $request->input('id');
Validator::make($request->post(), [
    'username' => ['required', "unique:users,name,$userId"],
]);

Since Laravel uses $userId here to query that database and $userId is not escaped, it will allow an attacker to perform an SQL injection.

Case 1: Making the validation rule optional

The simplest thing we can do here is send a request with ID = 10|sometimes, which alters the validation rule to required|unique:users,username,10|sometimes and allows us to skip the username in the request data. Depending on your application's business logic, a bypass like this might create a security issue.

Case 2: DoS the server by creating an evil regex validation rule

Another attack vector is to create an evil regex validation rule that is vulnerable to a ReDoS attack and can take the app down. For example, the following request would consume a lot of CPU, and if multiple requests are sent concurrently it can cause a big CPU spike on the server.

PUT /api/users/1,id,name,444|regex:%23(.*a){100}%23

{
    "username": "aaaaa.....A_LOT_OF_REPEATED_As_aaaaaaaaaa"
}

Case 3: SQL Injection

The simplest SQL injection here would be to just add an extra validation rule that is querying the database, for example

PUT /api/users/1,id,name,444|unique:users,secret_col_name_here

{
    "username": "secret_value_to_check"
}

It is important to mention that, because unique lets us provide both a custom column name and values (values do not go through PDO parameter binding), the possibilities for SQL injection here are not limited to the simple attack vector mentioned above. For more details, check out Laravel Blog’s post "Unique Rule SQL Injection Warning".

The best prevention here is to not use user-provided data to create a validation rule.

RAW Query SQL Injection

DB::raw function is dangerous when developers don’t escape passed data. If you have to use DB::raw function for some custom query, make sure you escape the passed data via DB::getPdo()->quote() method.

XSS in Laravel Blade

Cross-Site Scripting can be very dangerous, for example an XSS attack in the admin panel can allow an attacker to inject a code like this:

Some text
<input onfocus='$.post("/admin/users", {name:"hacker", email:"hacker@example.com", password: "test123", });' autofocus />
test

This allows an attacker to create an admin user with their own credentials and take over the admin panel.

Laravel Blade protects from most XSS attacks, so for example an attack like this will not work:

// $name = 'John Doe
<script>
    alert("xss");
</script>
';
<div>{{ $name }}</div>

Blade’s null statement automatically encodes the output. So the server will send the following properly encoded code to the browser (which will prevent the XSS attack):

<div>John Doe&lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;</div>

But frameworks can’t handle all cases for developers.

Case 1: XSS via {!! $variable !!} Statement

Sometimes you need to output a text that contains HTML, and for it you will use {!! !!}:

<div>{!! $htmlDescription !!}</div>

In this case Laravel can’t do anything for you and if the $htmlDescription contains JavaScript code, it will be executed as-is and we will get an XSS attack.

Prevention tips:

  • If you can, avoid outputting user-supplied data without HTML encoding.
  • If you know the data can contain HTML, use HTML Purifier to strip JS and unwanted tags before outputting the content.

Case 2: XSS via a.href Attribute

If you are outputting a user-provided value as a link, here are some examples of how it can turn into an XSS attack.

// $user->website = "javascript:alert('Hacked!');";
<a href="{{ $user->website }}">My Website</a>

The alert('Hacked!') code will get executed when a user clicks the link.

Prevention tips:

  • Validate user-provided links; in most cases you only need to allow the http/https schemes.
  • As an extra layer of security, before outputting you can replace any link that does not start with an http/https scheme with something like a #broken-link value.

Case 3: XSS via Custom Directive

When you write a custom directive, don’t forget to use Laravel’s e function to escape any code that is user provided. An example of vulnerable code:

// Registering the directive code
Blade::directive('hello', function ($name) {
    return "<?php echo 'Hello ' . $name; ?>";
});

// user.blade.php file
// $name = 'John Doe <script>alert("xss");</script>';
@hello($name);

Mass Assignment

Mass Assignment Vulnerabilities

Example: a User model with:

protected $fillable = ['name', 'email', 'password', 'role'];

If in a Controller a developer uses something like $user->fill($request->all()); or $user->update($request->all());, a user/attacker can add an input with a role name and submit the form, thus setting a role, e.g. to "administrator".

Prevention tips:

  • Don’t use Mass Assignment
  • Pass to Model only fields that have been validated: $user->update($validator->validated());
  • Use whitelisting instead of blacklisting (prefer $fillable over $guarded, because it’s easy to forget to add a new column to $guarded when you add it to a Model)
  • Use $model->forceFill($data) method with caution, make sure passed data cannot be manipulated by the user
Copyright © 2026