Security is always a concern when you are developing a web application.
Not only do you need to think about the security features and vulnerabilities, but also about the possible issues that might appear during the process.
There are a lot of segments that need to be covered and taken care of if you want your application to be secure.
Fortunately, tools like the Laravel framework provide us with a lot of good practices and excellent features. So, if you are building your application using this framework, you can rest assured that the Laravel security package will deliver the results you want.
In this text, we are going to dive into these Laravel security features, and other out-of-the-box practices. We will take a close look into their implementation to understand how we can protect our application.
Table of contents
Code Injections
SQL Injections
In plain PHP, we need to bind all the parameters on SQL queries. But in Laravel, we have the query builder and Eloquent ORM that provides automatic protection against SQL injections by adding param binding by default. Even with this, you should watch out for malicious requests, like for example:
User::query()->create($request->all());
This code could lead to a mass assignment. In this case, a user can send a payload like this:
{
"name": "John Doe",
"email": "[email protected]",
"role_id": "admin"
}
Another code that could lead to the same issue could be:
$user->fill($request->all());
$user->save();
In this example, we are hydrating an eloquent model with all the data from a request and then saving it.
A malicious user can try with different payloads. Or, they can add extra inputs with different names and try to find a weak implementation like this.
Hopefully, with this example, we can see that we need to take care of mass assignments. We cannot trust any user request, because any user can open the browser inspector and add an input in a monolith or modify the payload from an API.
Laravel provides different ways to handle this:
Set Fillable Property
We can prevent mass assignment by adding explicitly the fields that a model contains by using protected properties, "fillable" or "guarded":
protected $fillable = ['name', 'email', 'password', 'role_id'];
In this case, we are adding explicitly the columns that a model contains. You can use the guarded property as an empty array. Personally, I do not like this approach as many projects have more than one developer and there is no guaranty that other developers would validate the data.
The forceFill() method can skip this protection, so take care when you are using this method.
Validate Request Data
You should validate any type of resource no matter where it came from. The best policy is to not trust the user. Laravel provides FormRequest so we only need to create one with artisan:
php artisan make:request UserRequest
You can define the rules to validate your requests:
public function authorize()
{
return $this->user()->check();
}
public function rules()
{
return [
'name' => ['required', 'string', 'min:5', 'max:255'],
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', Password::default()]
];
}
The authorize method must return a boolean. It is a convenient way to return an authorization validation before starting to validate the requested content. This is something to take in mind and it would apply in any route that has the middleware auth for web or sanctum/API if you are using token-based authentication.
The rules method returns an array with the rules that are validating your request. You can use a lot of rules out of the box or create your own custom rules. If you are interested to dive in deeper into this topic, you can find all the rules in the doc: https://laravel.com/docs/8.x/validation#available-validation-rules.
XSS Attack
This attack could be divided into two sections. The first one restricts special tags on the server and does not return special tags in the views.
Restrict Special Tags in the Server
You could use different approaches. PHP natively has some methods like strip_tags that only protect against HTML and PHP tags. You can even use a regex or use the PHP native method htmlentities() or filter_var both, although it does not protect completely against all the possible tags. In this case, my best recommendation is to use a specific package to solve this, like HTML Purifier.
Does Not Return Special Tags in the Views
If you are working with the Blade template engine, you should take care about how you are printing your data in your template:
<p>{{ $user->name }}</p>
The double mustaches syntax would protect you against XSS attacks by automatically escaping the tags for you.
<p>{!! $user->name !!}</p>
On the other hand, this syntax is dangerous. If you do not trust the data that could come, do not use it because the bang-bang syntax could interpret PHP.
Using Another PHP Template Engine
Laravel also provides an escape method that we use on any other template engine like Twig:
{{ e($user->name) }}
Using a Javascript Framework
Any modern Javascript framework automatically protects us to inject a script. VueJS, for example, has a v-html directive that already protects us against this type of attack.
Request Origin
In your application, you can get requests from multiple sites. It could be a webhook, a mobile application, requests from a Javascript project, etc.
In these cases, we should take a defensive approach. A lot of antiviruses are great examples that a non-trust list simply does not work as we cannot keep updating different origins and sites all the time. In this case, a trust list can be the best approach to only validate some origins.
In short, a trust list could work if we know the origins that we are going to allow. But what if we do not?
Maybe an unknown origin could try to send unauthenticated requests. In this case, Laravel once again provides a great tool out of the box. We can use the throttles middleware to protect a route or group of routes from malicious requests. This is one of Laravel's security best practices to consider.
Route::get('dashboard', DashboardController::class) ->middleware('throttle:3,10');
The param:3,10 represents that it allows 3 requests during 10 minutes. At the fourth request, it would throw an error 429 in the browser. If it is a request that has a content-type: application/json and accept: application-json, it would return a response with 429 status and a message with the exception.
You can go even further and add a RateLimiter on the app/Providers/RouteServiceProvider.php:
protected function configureRateLimiting()
{
RateLimiter::for('global', function (Request $request) {
return Limit::perMinute(1000);
});
}
Then in your route file, you can define a route like this:
Route::get('dashboard', DashboardController::class)->middleware('throttle:global');
If you want to dive deeper into the rate limiter, you can visit this resource. And if you want to get something more robust in terms of a trusts list, here is a great package for adding a white list implementation.
Do Not Trust Sites Without an SSL Certificate
A site that does not have an SSL certificate should not be allowed. No data should be sent without proper encrypted channels as this could lead to a potential man-in-the-middle attack where your data can be exposed.
Lastly, do not share session ids or cookies with insecure connections that do not use the HTTPS protocol. Doing so can also expose sensitive data.
Exposed Files
By default, Laravel only exposes the public directory. This is intended to avoid security breaches. Considering that any file that will be exposed can be accessed by anyone, you should avoid adding their sensitive data.
If you want to expose files to download, the best way to do this is by keeping the files on the storage directory and just adding a symbolic link on a public directory. Laravel provides a command to make it easier:
php artisan storage:link
Now, any file that your app stores in the storage directory will be available. Avoid adding manual permissions to any other directory as this could lead to a potential breach.
Weak Login Implementation
All the authentication workflow, register, forgot password, login, etc, are steps that require utmost attention. For example, if you return a specific message for any field that does not match in a login form, the attacker could know exactly when an email already exists in the database.
One of the strengths of the Laravel ecosystem is that they offer a lot of packages to work with authentication:
- laravel/ui - basic authentication, comes with blade views and bootstrap css
- laravel/breeze - basic authentication, comes with blade views or inertiaJS components, use tailwindcss for styles
- laravel/jetstream - basic authentication, user profile, 2FA, teams, comes for livewire and inertiaJS stacks, use tailwindcss for styles
- laravel/fortify - authentication backend logic without any ui preset
- laravel/passport - full JWT authentication (most of the time over engineer)
- laravel/sanctum - api tokens authentication with scopes
In this case, a good practice would be to use a package that meets your needs, has official support, and has contributions from the community.
The Right Configuration For Your Environment
Let’s imagine that you push your code to the production environment, and in your production .env file, you set the key APP_ENVIRONMENT=local and APP_DEBUG=true.
In this case, every time that your app throws an error, it would show the stack trace of the exception and it would probably reveal more than you would like.
A stack trace screen would appear to any potential attacker. The technology that is used on the project – the database table and its structure, and the application directory structure – shows there might be more vulnerabilities to explode. With this in mind, take care of the environment file values, but take special care of those two keys.
Software/Packages Updates
By the time your project dependencies get updated, the package authors or the community could find vulnerabilities, like a patch, for example. That is why is so important to update every package that your app has – at least as a production dependency.
You can update your packages by simple running a composer/npm command:
composer update npm update
This command updates the current package/dependencies version. If you want to update to a major release you can execute:
composer outdated
npm outdated
Password Vulnerabilties
Any password should be hashed. Luckily, Laravel provides more than one way to hash data:
bcrypt('LaravelIsCool');
Hash::make('LaravelIsCool');
The APP_KEY is used to encrypt and decrypt data, but it can also be used for signed routes too. This has no relation with hashes, so use it with confidence.
Prevent CSRF Attack
Laravel API security also goes the extra mile with a mechanism to protect the application against CSRF attacks. This type of attack is very difficult to replicate and we do not need to cover it as the framework does it for us.
A CSRF attack makes a request from another browser tab and tries to submit malicious requests to the application. Laravel protects us against these attacks. Every request generates a token that changes on every request. This token would be known only by the application and every request should have this token to validate that the request comes from the same server.
In blade, you can use the directive @csrf:
<form method="POST" action="/profile">
@csrf
<!-- Equivalent to... -->
<input type="hidden" name="_token" value="{{ csrf_token() }}" />
</form>
To exclude some requests that come from a webhook that was created outside of our application, there is a protected property $except in the VerifyCsrfToken middleware:
protected $except = [
'stripe/*',
'http://example.com/foo/bar',
'http://example.com/foo/*',
];
Prevent DOS Attack
These types of attacks can be divided into two popular categories:
DOS Attacks That Send a Lot of Requests
These attacks would send a lot of PHP requests that are not closed. The server responds to multiple requests until it cannot support more requests and the memory fails, resulting in our server going down. An example of these attacks could be a "slow loris" attack.
Laravel throttle middleware and RateLimiter help us to handle these attacks by IP. It's important to remember that in your app context, you can handle, but not stop requests from the outside world. You should need other dev-ops tools and server platform tweaks to mitigate these attacks.
DOS Attacks That Send Large Files to Consume the Server Memory
Another variety of this attack could be in a public form. Maybe your application has some public form to submit a file. In this case, large files can exhaust the server memory. Keep in mind that the server should be serving data/resources to other users and handling this type of submits all at the same time.
To handle this attack, you can use the Laravel API security validator to validate the file from the request. Here is an example:
//file is exactly 512 kilobytes..
'photo' => ['mimes:jpg,bmp,png', 'file', 'size:512']
// file max size is 512 kilobytes..
'photo' => ['mimes:jpg,bmp,png', 'file', 'max:512']
Additional Security Tips
Here is a list of tips that could increase your app security:
Use a Honeypot on Any Public Form
Any public form can be submitted by anyone. To avoid malicious requests from bots, you can set a hidden input. The bots would fill the input (a normal user should not fill a hidden input), and then you can use the prohibited validation rule from Laravel validator:
// this input should never comes in the request
'honey_pot_field' => ['prohibited'],
Constantly Change Your APP_KEY Value
This can be challenging if you have data encryption store models. In this case, I suggest using a package that handles it for you: https://github.com/rawilk/laravel-app-key-rotator. The package rotates the APP_KEY, decrypting and encrypting again all models that were encrypted.
Send an Email When a User Updates a New Email Account
Laravel provides a feature to send an email to verify a new account with new user registration. However, when the same user changes an email account, it does not verify the new email address. This process could be automated by a package: https://github.com/protonemedia/laravel-verify-new-email.
The same applies to password changes.
Register SSH Credentials on Your Server Cautiously
Try to connect with SSH only from places where your connection is "secure." Avoid public wifi connections.
Set Tokens Lifetime
For Laravel Passport:
In your app/providers/AuthServiceProvider.php, you can set a specific lifetime for every token:
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Passport::routes();
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}
For Laravel Sanctum:
Just publish the sanctum config file and change the value. The time would be set in minutes:
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
'expiration' => null,
Use Authorization Features
Laravel security provides a native system to authorize users on any action. Actions that are not related to any specific model are typically covered by Gates, and the rules that are tight to a model could be covered by Policies. Laravel provides a lot of ways to apply these rules across the app layers. You can apply an authorization rule on:
- Routes, by using your gates and policies as middlewares.
- Controllers, using $this->authorizeResource() in the constructor of resource controllers.
- Controllers, using a more granular validation with $this->authorize() method.
- Any place, (models, custom services, resources, gates, policies, etc) where you have available the authenticated user with the method $user->can() or $user->cannot().
- In Blade views, by using the directive @can and @cannot.
- If you are using any javascript template system you can set an array of permissions in your user or any other model, to get pretty similar functionality, as any policy, the typical actions like viewAll , view , create , update , destroy, here an example:
<button v-if="$page.props.auth.user.permissions.admin_action.create">Admin Action</button>
<td v-if="product.permissions.view">{{ product.name }}</td>
Conclusion
This introduction to the security aspects of Laravel allows us, the Laravel developers, to better understand how the framework already protects us from many vulnerabilities.
It also shows us the additional value of Laravel by allowing us to focus our time on development and not on solving common security problems.
An additional advantage is that we can see how working with a tool with such a wide ecosystem allows us to add third-party packages to solve specific problems that other developers have already faced before.
And if you're in a reading mode and want to advance your Laravel skills, take a look at the following resources: