Securing your Laravel application: A comprehensive guide
- Article tags
Known for its elegant syntax, extensive documentation, and powerful features, Laravel has helped countless developers build robust and scalable web applications with ease.
With great popularity comes great responsibility, so securing Laravel applications has never been more crucial. From small personal projects to large enterprise apps, no web application is immune to security risks and vulnerabilities.
Laravel consistently leads the pack: Laravel has consistently been the most popular PHP framework throughout the given period. Its popularity increased steadily from 38% in 2017 to 50% in 2021. This growth suggests a strong developer preference for Laravel due to factors like ease of use, elegant syntax, and extensive ecosystem.
As someone who has worked with the Laravel framework for years, I've seen firsthand the importance of taking security seriously. I've seen how simple mistakes lead to disastrous consequences, and I've also seen the benefits of a secure and well-maintained Laravel application.
For example, I once worked on a Laravel-based e-commerce platform whose code had several vulnerabilities. A malicious attacker was able to exploit them and steal sensitive customer data, resulting in a costly and damaging data breach. This experience taught me the importance of regularly conducting security audits and keeping Laravel applications up-to-date with the latest security patches.
On the flip side, I also worked on a Laravel-based project where the development team prioritized security from the outset. They implemented best practices like two-factor authentication (which may prevent broken authentication exploits), data encryption, and strict input validation. The resulting application was not only secure but also performed exceptionally well under heavy traffic.
If you’re as determined as I am to secure your Laravel applications, let me take you on a deep dive into:
The importance of keeping the Laravel framework and its dependencies up-to-date
Implementing secure authentication
Protecting against SQL injection
Securing against cross-site scripting (XSS) attacks
Securing APIs, implementing secure file uploads
Enforcing access control
Keeping credentials safe
Configuring debug mode and app environment.
I'll cover each of these topics in detail and give you real-world examples of how to implement these security measures in your own Laravel applications.
By the end of this article, you'll have a solid understanding of how to build secure, robust, and scalable Laravel applications that can withstand even the most determined attackers.
So, whether you're building a small personal project or a large enterprise application, let’s explore the world of Laravel security and learn how to keep our applications secure and safe to use!
Keeping Laravel and its dependencies up-to-date
When working with Laravel, it's crucial for developers to stay updated with the latest versions and dependencies to prevent security vulnerabilities.
Unfortunately, I've encountered too many clients who neglect to update their Laravel versions, often running versions that are several years old. This passivity can introduce vulnerabilities into the application, leaving it open to potential security threats.
While the world of security is constantly evolving, it's important to remember some serious flaws that Laravel has had in the past:
CVE-2021-43617 – had a significant impact on Laravel 8, as it failed to fully prevent the upload of executable PHP files. This was caused by a lack of verification for .phar files within the ValidatesAttributes.php file in the Validator namespace
CVE-2022-25838 – was a problem identified in Laravel Fority with Versions <1.11.1 which made it possible for users to reuse codes within a limited time window, which raised doubts about the one-time (OT) aspect of the Time-Based One-Time Password (TOTP) mechanism
CVE-2018-6330 – was an old vulnerability present in Laravel 5.4.15 which allowed for Error-based SQL injection in save.php through the dhx_user and dhx_version parameters, opening the application to threat vectors.
Updating Laravel and its dependencies is vital to the security and stability of your Laravel application. Outdated software can leave your application vulnerable to security threats, bugs, and performance issues. Upgrading to the latest versions of Laravel and its dependencies is highly recommended as they contain core security patches, bug fixes, and performance improvements, notably:
Laravel 5.4.21 – addressed a critical vulnerability in the framework’s encryption mechanism
Laravel 5.5.27 – fixed a potential remote code execution vulnerability
Laravel 5.5.40 and 5.6.29 – addressed an XSS vulnerability in the framework’s error page handler
Laravel 5.6.33 and 5.7.15 – fixed an issue that allowed for arbitrary file upload via the Filesystem component
Laravel 5.8.12 – patched a potential DDOS vulnerability caused by the framework’s URL validation mechanism
Laravel 6.4.1 – fixed a remote code execution vulnerability exploitable using the Signed URLs mechanism
Laravel 7.10.2 – addressed a potential SQLi vulnerability in the framework’s query builder
Laravel 8.12.3 – patched a potential XSS problem arising in the error page handler
Laravel 8.15.0 – addressed a security issue in the framework’s input validation that made it possible to bypass validation rules
The process is quite straightforward:
Make sure you're running the latest stable version of Laravel and a compatible PHP version (Laravel 10 requires at least PHP 8.1.x to run). Check out the upgrade guide that Laravel provides.
Once you've updated Laravel, verify that your dependencies are up-to-date as well. To do this, run the command
composer outdated
to check for outdated packages, and then run composer update to update them.Follow best practices when updating Laravel and its dependencies. One of the most important things is to test your application thoroughly after any updates to ensure everything is working as expected. You can do it either manually or through automated testing. I suggest having automated tests covering at least 90% of your application. That way, no matter what you change, you know what and where something breaks.
Stay informed about any security vulnerabilities or critical CVEs in Laravel and its dependencies. Laravel has a great community of developers and maintainers who release regular updates to address these issues. We resonate with this approach as our team at Pentest-Tools.com understands the importance of staying vigilant against security threats and is dedicated to vulnerability detection and safeguarding you against their real-world impact.
At its core, Laravel tries to offload the responsibility of security from the developer as much as possible, creating plug-and-play and simple solutions, just like the out-of-the-box authentication system. It's essential to keep this system up-to-date, even more so if you are using a system like Jetstream or Breeze, to ensure its effectiveness against ever-evolving threats. Laravel also offers CSRF protection and other security features to prevent HTML parameter tampering.
To illustrate the importance of keeping Laravel and its dependencies up-to-date and safe, let's take a look at some examples, which we are going to review in detail later.
SQL injection and Cross-Site Scripting (XSS) attacks are two of the most common types of security vulnerabilities in web applications. Laravel provides built-in protection against SQL injection and XSS attacks, but these protections are only effective if you keep Laravel up-to-date and use it properly.
Implementing secure authentication
In today’s digital landscape, the importance of implementing secure authentication is crucial. With an increasing number of cybercriminals looking to exploit stolen credentials and the ever-growing number of data breaches, organizations need to focus on robust cybersecurity measures to protect sensitive information and ensure the integrity of their systems.
Source: Statista
Credential stuffing attacks have become a significant threat, with billions of compromised credentials circulating on the dark web. Despite the low success rate of these attacks, their financial and reputational impact on the organization is considerable. Companies are facing mounting pressure from both regulators and the public to put in place proper security measures that can prevent credential stuffing and other forms of cyberattacks.
To mitigate these risks, organizations can adopt various methods:
Yet, defending against stuffing represents only one aspect of a broader cybersecurity strategy. Initial access, as defined by the MITRE ATT&CK framework (TA0001), encompasses the different entry points attackers use to infiltrate systems, such as:
Phishing (T1566)
Drive-by compromise (T1189)
Software supply chain compromise (T1195)
Trusted business channels compromise (Freestyle, could not find the T0000 for this)
Understanding and addressing these entry points enables organizations to strengthen their security posture and prevent unauthorized access by hackers.
In Laravel, you can easily implement authentication using the built-in features of the framework. However, it's important to follow best practices to ensure that your implementation is as secure as possible.
1. Use strong passwords
One of the key aspects of authentication security is using strong and unique passwords. Make sure your users are required to choose a strong password that meets certain criteria, such as a minimum length and a combination of uppercase and lowercase letters, numbers, and symbols. Laravel includes built-in password validation rules that you can use to enforce these requirements:
<?php
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\Password;
$validator = Validator::make($dataToValidate, [
'password' => [
'required',
'confirmed',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
],
]);
The uncompromised
method uses https://haveibeenpwned.com to determine if the password has been exposed and does it without leaking user data.
By default, the password is compromised if it appears at least once in there. You can loosen your validation simply by telling it how many times it should appear to be considered compromised:
Password::uncompromised($numberOfAppearances);
2. Define your custom password validators
You or your company may have more policies regarding password validation, and Laravel may not have them all built-in, so, instead of that, you can define your own validation rules like this:
Run php artisan make:rule Rulename
to create a new validation rule class:
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class ContainsSymbolsInProduction implements ValidationRule
{
/**
* Run the validation rule.
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$rule = Password::min(8);
if (!$rule->symbols()) {
/**
* :attribute will be replaced with the $attribute name
* if $attribute has the value "Nickname" the $fail error
* will return "The nickname must contain symbols."
*/
$fail('The :attribute must contain symbols.');
}
}
}
The neat thing about all of this is that after you create this $validator
object, you have to run ->validate()
on it to check every validator against the given data.
This implies that, instead of creating a separate rule, you can use the validate($attribute, $value, $fail)
straight into the rule.
Use this approach only when the rule is not used anywhere else. Otherwise, you should create a custom validation rule class.
<?php
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\Password;
$validator = Validator::make($dataToValidate, [
'password' => [
'required',
'confirmed',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(),
function (string $attribute, mixed $value, Closure $fail) {
$rule = Password::min(8);
if (!$rule->symbols()) {
/**
* :attribute will be replaced with the $attribute name
* if $attribute has the value "Nickname" the $fail error
* will return "The nickname must contain symbols."
*/
$fail('The :attribute must contain symbols.');
}
}
],
]);
/**
* And to run our validation rules. This will either return validated data
* Or throw a new ValidationException()
*/
$validated = $validator->validate();
With this, you should be able to create any validation rules your heart and company desire! This only adds to the fact that Laravel is powerful only when used right. In the wrong hands, you can be vulnerable even with the latest and best Laravel framework.
On another note, the definition of this method is as follows:
Validator::make(array $data, array $rules, array $messages);
// $data — the concrete data we want to validate
// $rules — the rules for each field we want to validate
// $messages — the message to be displayed for a data field when it fails a certain rule
3. Enable two-factor authentication
Two-factor authentication (2FA) adds an extra layer of security to your authentication process by requiring users to provide a second factor, such as a code generated by an app or sent via SMS.
Laravel provides support for two-factor authentication out of the box, and you can use a package like Laravel Jetstream to implement it into your application. The Lararavel documentation recommends installing Jetstream only in new apps.
Installing Jetstream in your application can be as easy as plug-and-play. First, you have to install the Jetstream package:
composer require laravel/jetstream
Then you can run this:
php artisan jetstream:install livewire|inertia
The command accepts an argument where you can state if you want to use either Livewire or Inertia for your front end. To finish the installation, just run the following:
npm install
npm run build
php artisan migrate
And now, everything should be installed and you no longer have to spend hours implementing a custom version for this.
4. Using secure session management
This is another critical aspect of authentication security. Laravel provides a powerful and secure session management system that can help you prevent common attacks like session hijacking and session fixation.
You can configure your session settings in the session.php configuration file.
<?php
return [
'secure' => env('SESSION_COOKIE_SECURE', true)
'http_only' => env('SESSION_COOKIE_HTTPONLY', true),
'same_site' => 'lax',
];
5. Implement rate limiting
This can help you prevent brute-force attacks on your authentication system. Laravel includes a built-in rate limiter that you can use to restrict the number of attempts a user can make to log in to your application within a certain time period.
The default configuration is available in the RouteServiceProvider
file from app/Providers
, inside the configureRateLimiting()
where you can set both the default application cache and the cache driver for the rate limiter.
Here, you’ll want to do two things: increment attempts and clear attempts.
Incrementing attempts
There are four important methods for the RateLimiter
facade:
1) hit($rateLimitingKey)
— increments the rate-limiting key attempts with one
2) tooManyAttempts($rateLimitingKey, $maxAttemptsPerMinute)
— determines if a rate-limiting key has reached its maximum attempts allowed in a minute
3) remaining($rateLimitingKey, $maxAttemptsPerMinute)
— retrieves the remaining retries available in the current minute
4) availableIn($rateLimitingKey)
— retrieves the number of seconds after which a user can attempt the action again
Clearing attempts
Here there’s only one method you need for the RateLimiter
facade:
clear($rateLimitingKey)
— takes a rate liming key and clears all the attempts for it in that minute
6. Use CSRF protection
Bad actors use cross-site request forgery (CSRF) attacks to trick users into performing malicious actions on your application without consent. Laravel includes built-in CSRF protection that you can leverage to prevent these attacks. If you want to see an attack involving the CSRF token, check out our article on exploiting Gitlab CE/EE RCE.
Here’s the code snippet for CSRF protection, which you can introduce into your forms:
<form method="POST" action="/example">
@csrf
<!-- Form fields go here -->
</form>
One more precaution is to check your app/Http/Middleware/VerifyCsrfToken.php
file. It looks like this:
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
The $except
property is an array that includes all routes which match a pattern that will be exempt from CSRF verification. You should almost never have any exceptions for any routes in your application.
If there is an exception, you may want to find another way of sending the CSRF token for that route, so you do not leave yourself open to this vulnerability.
7. Use SSL/TLS
It's important to use SSL/TLS encryption to protect your users' credentials and prevent eavesdropping on their communications with your application. Laravel supports SSL/TLS out of the box, and you can enable it by configuring your web server to use HTTPS.
How to configure an NGINX server
Let’s take a look at the server configuration before we use HTTPS:
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name _;
location / {
try_files $uri $uri/ =404;
}
}
But it does not stop here.
You can access your application with HTTP, but make sure to redirect it to HTTPS. You can easily do it by configuring the HTTP to redirect with a 301 code:
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 301 https://$host$request_uri; #the redirect from HTTP to HTTPS
}
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl_certificate /etc/nginx/certificate/nginx-certificate.crt;
ssl_certificate_key /etc/nginx/certificate/nginx.key;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name _;
location / {
try_files $uri $uri/ =404;
}
}
How to set up an Apache server
As before, here’s what the configuration looks like by default for the HTTP protocol:
<VirtualHost *:80>
ServerAdmin admin@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
For HTTPS, make the following changes:
Set up the server to listen to 443
Turn on the SSLEngine
Use the SSLCertificateFile and SSLCertificateKeyFile
Here’s what the final result looks like:
<VirtualHost *:443>
ServerAdmin admin@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLEngine on
SSLCertificateFile /etc/apache2/certificate/apache-certificate.crt
SSLCertificateKeyFile /etc/apache2/certificate/apache.key
</VirtualHost>
And, for the best part, you can, once again, redirect from HTTP to HTTPS following a method similar to the NGINX server:
<VirtualHost *:80>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R=301,L] #Where the redirect happens
</Virtualhost>
<VirtualHost *:443>
ServerAdmin admin@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLEngine on
SSLCertificateFile /etc/apache2/certificate/apache-certificate.crt
SSLCertificateKeyFile /etc/apache2/certificate/apache.key
</VirtualHost>
Protecting against SQL injection
SQL injection is a type of security vulnerability where an attacker can exploit a web application's vulnerability and execute malicious SQL statements. The consequences of a successful SQL injection attack can be disastrous, ranging from data theft to complete system compromise.
Laravel provides several mechanisms to mitigate SQL injection attacks, but not knowing how to use them can leave you as vulnerable as ever.
What is SQL injection and how does it work?
SQL injection is a type of attack that involves injecting malicious SQL statements into an application's database query. This can lead to unauthorized access to sensitive data or even damage to the system.
For example, let's consider a scenario where a web application uses an SQL query to retrieve user data based on a search term. The URL query looks like this:
https://secure.app.com/users?userId=1
If the application doesn't properly sanitize the search term, an attacker can craft an SQL query that executes malicious code.
Let’s assume there is a users table, so why not try our luck with it? If the SQL is running, here’s what it displays:
SELECT * FROM users WHERE id = 1;
If the website does not sanitize its input, you can mess with the value of userId and see if the server will execute it:
https://secure.app.com/users?userId=1'+OR+1=1--
This translates to the following SQL query:
SELECT * FROM users WHERE id = 1 OR 1=1--
Therefore, you will get everything from the users’ table where the id = 1 or where 1=1, so... all the data. Yes, it was that easy!
The attacker will then gain access to sensitive information, such as usernames and passwords (hashed - most of the time), or even take control of the database.
How to prevent SQL injection in Laravel applications
Laravel offers several mechanisms to prevent SQL injection attacks. Both Eloquent and the Query Builder provide protection against SQL injection. Except that you can create RAW SQL Statements, which is not recommended.
If you really want to write the RAW SQL Query, try prepared statements, which add a layer of protection because they escape the variables before executing the SQL Query.
Use prepared statements and parameterized queries such as:
$users = DB::select('SELECT * FROM users WHERE name = ?', [$name]);
In this example, the “?” placeholder represents the input variable $name
. Laravel will automatically escape the input variable to prevent SQL injection attacks.
Alternatively, you can use Laravel's query builder.
I consider this the best of both worlds, where you can write your query and not have a large layer of abstraction.
Laravel's query builder is a powerful tool that allows you to build SQL queries using an object-oriented syntax. The query builder provides several methods you can use to sanitize input data and prevent SQL injection attacks.
Here is an example of how to use Laravel's query builder:
<?php
$users = DB::table('users')
->where('name', '=', $name)
->get();
$products = DB::table('products')
->where('launch_country', 'USA')
->orWhere('product_name', 'RPG')
->count();
In this example, the where method is used to filter the query based on the input variable $name
. Laravel will automatically escape the input variable to prevent SQL injection attacks.
Another example is where we get the number of products that are either launching in the USA or have the product name "RPG".
The best part about the query builder is that it’s open to extension, so you can write your custom methods on it if you need to!
Securing against cross-site scripting (XSS) attacks
XSS (Cross-Site Scripting) attacks are a type of web security vulnerability that allows attackers to inject malicious scripts into web pages that other users can see. This type of attack exploits known vulnerabilities in a web application's input validation and output encoding.
In Laravel, there are several best practices you can follow to prevent XSS attacks.
Always validate user input to ensure it’s in the expected format and does not contain any malicious code.
Here are some examples of how to implement XSS protection in Laravel:
Laravel's Blade templating engine provides built-in protection against XSS attacks. Blade automatically escapes any user-generated content by default, meaning that any HTML or JavaScript tags will be converted to their equivalent HTML entities.
For example, the following code would be safe to include in a Blade template:
<p>{{ $userInput }}</p>
If you need to include unescaped content in your templates, you can use the {!! !!}
syntax to output the raw HTML. However, you should only use this for trusted content and ensure that any user-generated content is properly sanitized before being included in your templates.
For example:
<p>{!! $trustedInput !!}</p>
Laravel also provides the e()
helper function, which you can use to manually escape user input. This is useful if you're working with a string outside of a Blade template.
For example:
$escapedInput = e($userInput);
Validate all user input to ensure that it conforms to expected formats and does not contain any malicious code. Laravel's built-in validation tools can help with this, by providing a range of validation rules which you can use to check input values.
Here’s how:
<?php
$validatedData = $request->validate([
'name' => 'required|string',
'email' => 'required|email',
]);
CSRF protection
Laravel's built-in CSRF (Cross-Site Request Forgery) protection prevents attackers from tricking users into executing unintended actions on your application. This protection works by adding a CSRF token to forms and requests, and verifying that the token is valid before actually executing it.
To use Laravel's CSRF protection, you can include the @csrf
directive in your Blade forms:
<form method="POST" action="/example">
@csrf
<!-- Form fields -->
</form>
Content security policy (CSP)
Laravel also includes support for CSP headers, which can help prevent cross-site scripting attacks by specifying which sources of content your application can load. You can do this by adding a Content-Security-Policy
header to your application's HTTP responses.
Here's an example of setting up a CSP policy that only allows scripts from the same domain to be executed:
$response->header('Content-Security-Policy', "script-src 'self'");
By using these built-in security features, you can greatly reduce the risk of cross-site scripting attacks in your Laravel application.
Securing Laravel APIs
In every application, securing your API represents a crucial point because it holds sensitive data and you want to avoid unauthorized access. Not doing so can lead to data breaches, revenue loss, or, even worse, damage to the company’s reputation.
Case in point, in 2022, several large data breaches happened: Neopets which impacted 69 million users, Shangai COVID app which affected 48.5 million users, and the Bangkok Hospital breach which exposed 38.9 million patients. And the list goes on and on. These examples remind us about the importance of implementing proper security measures to keep data safe.
API security threats
As an ever-evolving playground, threat actors will continue to develop and deploy new exploits against Laravel apps, so it’s worth mentioning the most common vectors of attack they use when targeting an API.
Injection attacks — where the attacker inserts malicious code/commands into the API request. These attacks can target a variety of systems and can lead to data theft, modification, or even deletion.
Man-in-the-middle attacks — where an attacker intercepts and modifies the API request and response between the client and the server. This way, an attacker can steal data such as login credentials, credit card information, etc. - without being easily detected.
Broken authentication — weak authentication can lead to attackers gaining access to user accounts and stealing their data quickly or impersonating them to escalate their privileges.
Insufficient logging and monitoring — not much of an attack but a security threat nonetheless. This makes it difficult to detect and respond to any security incident or track them.
3 basic security measures to apply to protect Laravel APIs
By implementing these three measures, your application should be able to deter most bad actors from even attempting an attack.
Rate limiting APIs
This is an important security aspect because it involves limiting the number of requests a user can make to an API within a specified amount of time (usually 1 minute), which can prevent misuse, abuse, and DDoS attacks.
To implement this in Laravel, you can do it with Laravel’s built-in rate limiter.
Laravel provides out-of-the-box rate limiting for API routes. The configuration is inside the boot
method of your RouteServiceProvider
:
<?php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
The for()
method of the RateLimiter
facade accepts a name and a closure which returns the number of requests that a user can make in a given amount of time. In this case, a user can make 60 requests in a minute before the rate limiter will deny them further access.
You can tweak the code inside this closure to fit your needs. Other methods that you can use, other than perMinute()
, are perHour()
, perDay()
and perMinutes($minutes)
. The latter gives you the option to precisely choose the decay time.
You can also define custom rate limiting for a particular route or group of routes. Let’s say that your application has a route that consumes a lot of resources and you want users to only be able to access it twice per day. First, add a new call to RateLimiter::for()
inside the boot
method of your RouteServiceProvider
, where you pass a fitting rate limiter name:
<?php
RateLimiter::for('process-data', function (Request $request) {
Limit::perDay(2)->by($request->user()->id);
});
Then, you can add the throttle
middleware to your route, inside routes/api.php
(or any other routes file), mentioning the rate limiter name:
<?php
use App\Http\Controllers\ProcessDataController;
use Illuminate\Support\Facades\Route;
Route::post('/path/to/your/route', [ProcessDataController::class, 'store'])
->middleware(['throttle:process-data']);
Use bearer tokens
This is a popular method to authenticate when using APIs. The users must provide a token to authenticate themselves before accessing protected resources.
To use a bearer token in the Laravel framework, first install Laravel Sanctum. Then generate one key using the token()
method provided by the HasApiTokens
trait. The trait comes with Laravel Sanctum and you can add it to your User
model.
After doing this, you can easily generate a token like this:
<?php
$user = User::find($userId);
$token = $user->createToken('token-identification-key')->accessToken;
Next, add the authentication middleware to the routes you want to protect:
Route::middleware('auth:sanctum')->...
Or to a group of routes:
Route::group(['middleware' => 'auth:sanctum', 'alias' => 'v1'], function() {
Route::get(...);
});
And now, when you make an API call to these routes, make sure you include a token as a Header in the request in the form of Authorization: Bearer <generated_token>
Logging configuration
As I said, this might be one of the most important aspects of security as it helps better track security threats.
The first step is to configure the logging settings that Laravel has by default. They are available in config/logging.php
. By default, the logs are written in storage/logs
, but you can change this to wherever you like and also add logging channels like Sentry.
Attach a logger to our APIs so you can create a middleware for this:
php artisan make:middleware LogApiRequestsMiddleware
And, inside this, you can do the following:
<?php
public function handle(Request $request, Closure $next)
{
$response = $next($request);
Log::info($request->route(), [
'url' => $request->fullUrl(),
'method' => $request->method(),
'status_code' => $response->getStatusCode(),
'response_body' => $response->getContent(),
'user_agent' => $request->header('User-Agent'),
'ip_address' => $request->ip(),
]);
return $response;
}
The final step is to register the middleware in the app/Http/Kernel.php middleware array and it’s all set:
<?php
protected $middleware = [
// ...
\App\Http\Middleware\LogApiRequestsMiddleware::class,
];
Enforcing access control
Ensure that only authorized users can access certain resources and perform certain actions. Luckily, Laravel provides built-in functionality for this as well, so you don’t have to implement anything manually!
One way is to use the middleware filtering mechanism:
Route::middleware(['auth', 'can:view'])->...
The other option is to activate the authorization policies. These are classes that define a set of rules which determine whether a user can perform a specific action on a given resource:
<?php
class BlogPolicy
{
public function edit(User $user, Blog $blog)
{
return $user->id === $blog->userId;
}
}
And that’s it!
With these mechanisms in place, you can easily enforce access control in your Laravel application to ensure that only authorized users can access certain resources or actions!
Keep sensitive credentials out of the source code
When developing your app, you will most likely need to use external services, like the commonly-used database, email server, Redis, or AWS, or something more specific to your needs. These services will often require some credentials to connect to.
The easiest way to store these credentials, but definitely one of the worst ways from a security standpoint, is in the source code. This is wrong for various reasons:
Some developers may not need to know and therefore should not know those credentials, as per the principle of least privilege, and it will be impossible to share the source code with them without revealing the credentials
If source code is somehow leaked, for example from a remote repository, the credentials will also be leaked
So the problem arises: what is the best way to store them securely?
Luckily, Laravel comes with a solution: environment configuration using the .env file and the env()
method.
The .env file should be located in your application’s base directory. You should place all sensitive credentials inside it. Variables should be stored as KEY=value
, one on each line.
You can retrieve values from the .env file with the env()
function. For example, let’s say you’re using AWS and you need a secret key to connect to it. After you have defined AWS_KEY
inside .env, you can access it in the source code with env(‘AWS_KEY’)
.
The .env file should not be added in source control, so don’t forget to add it to .gitignore. Communicate these secrets via other secure channels and only with people who actually need them.
Disable debug mode in production and set the proper app environment
Debug mode and the app environment tell the app how to behave in different scenarios. Most often, they are used to differentiate between local and production environments.
Debug mode
Debug mode tells Laravel whether it should display full debugging information when errors occur. Debug mode is configured in your app.php configuration file. Instead of directly changing it here, you should change it in the .env file, by changing the value of APP_DEBUG
to true
, so that it’s not included in source control and can be configured differently in every environment.
<?php
return [
'debug' => (bool) env('APP_DEBUG', false),
];
APP_DEBUG=true
When developing or debugging the app, this will show snippets of code, request parameters, routing info, and more in the browser whenever an exception is thrown. This type of information can be dangerous if unauthorized users obtain access to them.
For demonstration, I will intentionally generate an error in a controller, specifically a Division by zero error:
<?php
namespace App\Http\Controllers;
class HomeController extends Controller
{
public function index()
{
$num = 0 / 0;
return view('welcome', ['num' => $num]);
}
}
If I leave debug mode on, this is what the browser will show when I access this route:
The exception message is exposed, along with a bunch of the source code. On the left, you can see the whole stack trace and inspect each file in it. Obviously, this is a big problem, since it can reveal potential flaws in your source code of which an attacker can take advantage.
And if you scroll down, you can see even more info about the app, like PHP and Laravel versions.
However, if I leave debug mode off, a 500 | SERVER ERROR message is the only thing that we will see.
Debug mode can be very useful in a development environment – but never leave it on in production.
Laravel-health is a package from Spatie that warns you, via email, Slack or other channels, of different problems in your app, including if you left debug mode enabled. To use it, simply install the package with Composer, follow their installation instructions and set up a new service provider containing your desired checks.
<?php
use Spatie\Health\Facades\Health;
use Spatie\Health\Checks\Checks\DebugModeCheck;
Health::checks([
DebugModeCheck::new(),
]);
App environment
The app environment is also configured in the app.php config file, but you should also change it from the .env file. You can leverage it to make your app behave differently in certain environments. You can set any value you want for it, however, some common ones are production, local and staging. The environment value can be accessed with App::environment()
, or you can check a specific one directly with App::environment($environmentToBeChecked)
.
For example, let’s say that you don’t want developers to have to go through the hassle of verifying their 2FA in a local environment. You can achieve this by setting the value of APP_ENV
to local and, in the source code, checking the environment to determine whether the 2FA validation logic should run. Note that this is just an example and you should do this only if you deem it safe.
<?php
use Illuminate\Support\Facades\App;
if (!App::environment('local')) {
// 2FA logic
}
And now, let’s say that the app environment is also set to local in production because nobody remembered to change it. The developer writing this code doesn’t have access to the environment variables in production (hopefully) and they may assume that it’s correctly configured. Therefore, this code will end up running in production and completely remove the 2FA functionality!
So, it’s a best practice to properly set the app environment as soon as you deploy the app for the first time.
Level up your Laravel security
Securing your application is an ongoing and crucial process that requires you to be vigilant and adaptive.
Through my years of experience, I learned a few key lessons that have significantly improved the way I handle the security of my applications:
Never take shortcuts – remember that CSRF token exception that you overlooked? It is piling up both as security and technical debt, and it’s coming for you.
Continuously monitor and learn from security incidents – building and deploying is not the end of the line, you have to make sure the boat stays afloat. Monitoring can help you get insights into problems you overlooked during development, and now you have the chance to fix them so they don’t snowball.
Embrace the principle of least privilege – limit access rights and permissions for users. Always start from zero permissions and add them as needed to avoid scenarios where a super-admin account exists and somehow gets hacked.
Remember, keeping your application safe is an ongoing and never-ending process that you need to focus on if you’re going to lower the risks of cyber threats. Attackers are always on the move – and so should you.