Hi, I'm Valerio Barbera, software engineer, founder and CTO at Inspector.
Data validation is one of the fundamental features in any application and it is something developers manipulate almost every day. The value a software provides to users is often a function of the quality of data it is able to manage. Laravel ships with a lot of predefined validation rules you can immediately use in your controllers.
Anyway working on the Inspector backend we have identified some aspects of validating incoming data, which have an impact on the security and reliability of the application and also thanks to custom rules you can easily extend the validation layer of your app with functionalities provided by external services.
Let me start with a bit of context to clarify the role the validation layer plays in a backend service, then I’ll show you our implementations.
Laravel Validation layer
Data integrity and validation are important aspects of web development because they define the state of the app. If data are wrong, the application behaves wrong.
It's always important to validate data not only before storing them in the database, but before doing anything.
In the Laravel request lifecycle an HTTP request sent by a client first goes through middleware. Middleware deals with a mix of things between authentication and security.
Now, before the request enters the application, the data it carries must be validated.
There are two ways to accomplish data validation in Laravel: Inside the controllers, or using Form requests.
Validation in controller
The easiest way of validation is performing it directly in the controller. At the start of each controller method you can first validate data:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|min:3',
'email' => 'required|email|min:6',
]);
// here we know data are valid so we can pass them to database or other services
}
}
Laravel will take care to return a 422 response code to the client if data are not valid.
Use Form requests
If your validation rules are too complex you may want to encapsulate them in reusable classes avoiding messing up the controller.
Laravel provides the ability to wrap validation in a dedicated component called FormRequest.
First create a form request class:
php artisan make:request StoreUserRequest
Than move your validation logic inside the rules method of the request class:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|string|min:3',
'email' => 'required|email|min:6',
];
}
}
You can type hint this new request class in the controller method instead of the original request class so Laravel will apply the validation rules automatically, and remove the validation statement:
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreUserRequest;
class UserController extends Controller
{
public function store(StoreUserRequest $request)
{
// here we know data are valid so we can pass them to database or other services
}
}
Custom validation rules
Laravel provided a really well developed validation layer. It can be easily extended implementing custom rules to be reused in your code, or to extend the capability of your validation using external services.
Let me show you a real example with one of the custom rules we implemented in Inspector.
First create the class that represent a validation rule in Laravel:
php artisan make:rule SecurePassword
The idea is to verify if the password is in the list of well known insecure passwords. If it is, it will not pass the validation, forcing the user to use a less common string.
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class SecurePassword implements Rule
{
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return !in_array($value, [
'picture1',
'password',
'password1',
'12345678',
'111111',
...
]);
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'The chosen password is unsecure. Try again with a less common string.';
}
}
Integrate with external services
Talking about data validation there are a lot of SaaS services that can bring new capability in your validation layer in terms of security and reliability of the data collected.
I recommend you take a look at apilayer.com who provides a great set of REST services to deal with data.
In Inspector we use the mailboxlayer.com API to validate emails. The service is also able to detect fake email addresses, temporary addresses, and the actual existence of an email address using MX-Records and SMTP.
Add two configuration property to store the api keys of the new services in the config/service.php
file:
return [
...,
'mailboxlayer' => [
'key' => env('MAILBOXLAYER_KEY'),
],
'vatlayer' => [
'key' => env('VATLAYER_KEY'),
],
];
Create the custom rule:
php artisan make:rule EmailSpam
Here is the complete code of the rule:
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class EmailSpam implements Rule
{
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
if (app()->environment('local')) {
return true;
}
return !config('services.mailboxlayer.key') || $this->check($value);
}
/**
* Perform email check.
*
* @param string $email
* @return bool
*/
protected function check(string $email): bool
{
try{
$response = file_get_contents('https://apilayer.net/api/check?'.http_build_query([
'access_key' => config('services.mailboxlayer.key'),
'email' => '[mailbox-layer-account-email]',
'smtp' => 1,
]));
$response = json_decode($response, true);
return $response['format_valid'] && !$response['disposable'];
} catch (\Exception $exception) {
report($exception);
if (app()->environment('local')) {
return false;
}
// Don't block production environment in case of apilayer error
return true;
}
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'Invalid email address.';
}
}
Tips & Tricks
Validate borders
Based on my experience I can suggest you to always validate not only the minimum size of the incoming fields but also the maximum size.
Don't wait for database errors that truncate too long strings and help your users to understand the limits of each field by the error messages returned during validation.
Ask for the current password
Every critical action should require a password confirmation.
You should always prompt the user to type the current password to authorize actions that can compromise the account accessibility, like change email, and change password.
This feature will improve security because also having physical access to the computer with the Inspector dashboard opened on the screen, a malicious user can’t change access credentials without knowing the current password. He can’t shut you out.
Here is our implementation of the current password verification:
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class CurrentPassword implements Rule
{
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return Hash::check($value, Auth::user()->password);
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'Your current password is incorrect.';
}
}
New to Inspector?
Are you looking for a "code-driven" monitoring tool to identify technical problems in your applications automatically?
Get a monitoring environment specifically designed for software developers avoiding any server or infrastructure configuration.
Thanks to Inspector, you will never have the need to install things at the server level or make complex configuration in your cloud infrastructure to monitor your application in real-time.
Inspector works with a lightweight software library that you can install in your application like any other dependencies based on the technology you are using to develop your backend. Checkout the supported technology on our GitHub (https://github.com/inspector-apm).
Visit our website for more details: https://inspector.dev/laravel/