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 that Laravel can’t protect, 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 fromLaravel’s documentation.
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 -- "'
Resume: Do not pass user-controlled column names to Query Builder without whitelisting.
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 that we can do here is to send a request with ID = 10|sometimes, which will alter the validation rule torequired|unique:users,username,10|sometimes and will allow us to not skip the username in the request data, depending on your application business logic, a bypass like this might create a security issue.
Case 2: DDOS the server by creating an evil REGEX validation rule
Another attack vector here could be to create an evil Regex validation, that is vulnerable to ReDoS attack and DDOS the app. For example, the following request would consume a lot of CPU and if multiple requests sent concurrently can cause a big CPU spike on the server.
PUT /api/users/1,id,name,444|regex:%23(.*a){100}%23
{
"username": "aaaaa.....ALOT_OF_REPETED_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"
}
But important to mention since using unique we are able to provide both custom column name and values (values are not going through PDO parameter binding) possibilities of SQL injection here could be not limited to just a simple attack vector that is 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
Which will allow an attacker to create an admin user with his 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<script>alert("xss");</script></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 in some cases you know that the data can contain HTML, use HTML Purifier to clean the HTML from JS and unwanted tags before outputting the content.
Case 2: XSS via a.href Attribute
If you are outputting user provided value as a link, here are some examples on 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 on the link.
Prevention tips:
- Validate user provided links, in most cases, you need only to allow http/https schemas
- As an extra layer of security, before outputting you can replace any link that is not starting with http/https schema with some “#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 ans thus, set 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
$fillableover$guarded, because it’s easy to forget to add a new column to$guardedwhen you add it to a Model) - Use
$model->forceFill($data)method with caution, make sure passed data cannot be manipulated by the user
