Mass Assignment Vulnerabilities and Validation in Laravel
Introduction
The following article is an excerpt from my ebook Battle Ready Laravel, which is a guide to auditing, testing, fixing, and improving your Laravel applications.
Validation is a really important part of any web application and can help strengthen your app's security. But, it's also something that is often ignored or forgotten about (at least on the projects that I've audited and been brought on board to work on).
In this article, we're going to briefly look at different things to look out for when auditing your app's security, or adding new validation. We'll also look at how you can use "Enlightn" to detect potential mass assignment vulnerabilities.
Mass Assignment Analysis with Enlightn
What is Enlightn?
A great tool that we can use to get insight into our project is Enlightn.
Enlightn is a CLI (command-line interface) application that you can run to get recommendations about how to improve the performance and security of your Laravel projects. Not only does it provide some static analysis tooling, it also performs dynamic analysis using tooling that was built specifically to analyse Laravel projects. So the results that it generates can be incredibly useful.
At the time of writing, Enlightn offers a free version and paid versions. The free version offers 64 checks, whereas the paid versions offer 128 checks.
One useful thing about Enlightn is that you can install it on your production servers (which is recommended by the official documentation) as well as your development environment. It doesn't incur any overhead on your application, so it shouldn't affect your project's performance and can provide additional analysis into your server's configuration.
Using the Mass Assignment Analyzer
One of the useful analysis that Enlightn performs is the "Mass Assignment Analyzer". It scans through your application's code to find potential mass assignment vulnerabilities.
Mass assignment vulnerabilities can be exploited by malicious users to change the state of data in your database that isn't meant to be changed. To understand this issue, let's take a quick look at a potential vulnerability that I have come across in projects in the past.
Assume that we have a User
model that has several fields: id
, name
, email
, password
, is_admin
, created_at
, and updated_at
.
Imagine our project's user interface has a form a user can use to update the user. The form only has two fields: name
and password
. The controller method to handle this form and update the user might like something like the following:
class UserController extends Controller
{
public function update(Request $request, User $user)
{
$user->update($request->all());
return redirect()->route('users.edit', $user);
}
}
The code above would work, however it would be susceptible to being exploited. For example, if a malicious user tried passing an is_admin
field in the request body, they would be able to change a field that wasn't supposed to be able to be changed. In this particular case, this could lead to a permission escalation vulnerability that would allow a non-admin to make themselves an admin. As you can imagine, this could cause data protection issues and could lead to user data being leaked.
The Enlightn documentation provides several examples of the types of mass assignment usages it can detect:
$user->forceFill($request->all())->save();
User::update($request->all());
User::firstOrCreate($request->all());
User::upsert($request->all(), []);
User::where('user_id', 1)->update($request->all());
It's important to remember that this becomes less of an issue if you have the $fillable
field defined on your models. For example, if we wanted to state that only the name
and email
fields could be updated when mass assigning values, we could update the user model to look like so:
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
];
}
This would mean that if a malicious user was to pass the is_admin
field in the request, it would be ignored when trying to assign the value to the User
model.
However, I would recommend completely avoiding using the all
method when mass assigning and only ever use the validated
, safe
, or only
methods on the Request
. By doing this, you can always have confidence that you explicitly defined the fields that you're assigning. For example, if we wanted to update our controller to use only
, it may look something like this:
class UserController extends Controller
{
public function update(Request $request, User $user)
{
$user->update($request->only(['name', 'email']));
return redirect()->route('users.edit', $user);
}
}
If we want to update our code to use the validated
or safe
methods, we'll first want to create a form request class. In this example, we'll create a UpdateUserRequest
:
use Illuminate\Foundation\Http\FormRequest;
class UpdateUserRequest extends FormRequest
{
// ...
public function rules(): array
{
return [
'email' => 'required|email|max:254',
'name' => 'required|string|max:200',
];
}
}
We can change our controller method to use the form request by type hinting it in the update
method's signature and use the validated
method:
use App\Http\Requests\UpdateUserRequest;
class UserController extends Controller
{
public function update(UpdateUserRequest $request, User $user)
{
$user->update($request->validated());
return redirect()->route('users.edit', $user);
}
}
Alternatively, we can also use the safe
method like so:
use App\Http\Requests\UpdateUserRequest;
class UserController extends Controller
{
public function update(UpdateUserRequest $request, User $user)
{
$user->update($request->safe());
return redirect()->route('users.edit', $user);
}
}
Checking Validation
When auditing your application, you'll want to check through each of your controller methods to ensure that the request data is correctly validated. As we've already covered in the Mass Assignment Analysis example that Enlightn provides, we know that all data that is used in our controller should be validated and that we should never trust data provided by a user.
Whenever you're checking a controller, you must ask yourself "Am I sure that this request data has been validated and is safe to store?". As we briefly covered earlier, it's important to remember that client-side validation isn't a substitute for server-side validation; both should be used together. For example, in past projects I have seen no server-side validation for "date" fields because the form provides a date picker, so the original developer thought that this would be enough to deter users from sending any other data than a date. As a result, this meant that different types of data could be passed to this field that could potentially be stored in the database (either by mistake or maliciously).
Applying the Basic Rules
When validating a field, I try to apply these four types of rules as a bare minimum:
-
Is the field required? - Are we expecting this field to always be present in the request? If so, we can apply the
required
rule. If not, we can use thenullable
orsometimes
rule. -
What data type is the field? - Are we expecting an email, string, integer, boolean, or file? If so, we can apply
email
,string
,integer
,boolean
orfiles
rules. - Is there a minimum value (or length) this field can be? - For example, if we were adding a price filter for a product listing page, we wouldn't want to allow a user to set the price range to -1 and would want it to go no lower than 0.
- Is there a maximum field (or length) this field can be? - For example, if we have a "create" form for a product page, we might not want the titles to be any longer than 100 characters.
So you can think of your validation rules as being written using the following format:
REQUIRED|DATATYPE|MIN|MAX|OTHERS
By applying these rules, not only can it add some basic standard for your security measures, it can also improve the readability of the code and the quality of submitted data.
For example, let's assume that we have these fields in a request:
'name',
'publish_at',
'description',
'canonical_url',
Although you might be able to guess what these fields are and their data types, you probably wouldn't be able to answer the 4 questions above for each one. However, if we were to apply the four questions to these fields and apply the necessary rules, the fields might look like so in our request:
'name' => 'required|string|max:200',
'publish_at' => 'required|date|after:now',
'description' => 'required|string|min:50|max:250',
'canonical_url' => 'nullable|url',
Now that we've added these rules, we have more information about the four fields in our request and what to expect when working with them in our controllers. This can make development much easier for users that work on the code after you because they can have a clearer understanding of what the request contains.
Applying these four questions to all of your request fields can be extremely valuable. If you find any requests that don't already have this validation, or if you find any fields in a request missing any of these rules, I'd recommend adding them. But it's worth noting that these are only a bare minimum and that in a majority of cases you'll want to use extra rules to ensure more safety.
Checking for Empty Validation
An issue I've found quite often with projects that I have audited is the use of empty rules for validating a field. To understand this a little better, let's take a look at a basic example of a controller that is storing a user in the database:
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email',
]);
$user = User::create($this->validated());
return redirect()->route('users.show', $user);
}
}
The store
method in the UserController
is taking the validated data (in this case, the name
and email
), creating a user with them, then returning a redirect response to the users 'show' page. There isn't any problem with this so far.
However, let's say that this was originally written several months ago and that we now want to add a new twitter_handle
field. In some projects that I have audited, I have come across fields added to the validation but without any rules applied like so:
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email',
'twitter_handle' => '',
]);
$user = User::create($this->validated());
return redirect()->route('users.show', $user);
}
}
This means that the twitter_handle
field can now be passed in the request and stored. This can be a dangerous move because it circumvents the purpose of validating the data before it is stored.
It is possible that the developer did this so that they could build the feature quickly and then forgot to add the rules before committing their code. It may also be possible that the developer didn't think it was necessary. However, as we've covered, all data should be validated on the server-side so that we're always sure the data we're working with is acceptable.
If you come across empty validation rule sets like this, you will likely want to add the rules to make sure the field is covered. You may also want to check the database to ensure that no invalid data has been stored.
Conclusion
Hopefully, this post should have given you a brief insight into some of the things to look for when auditing your Laravel application's validation.
If you enjoyed reading this post, I'd love to hear about it. Likewise, if you have any feedback to improve the future ones, I'd also love to hear that too.
You might also be interested in checking out my 220+ page ebook "Battle Ready Laravel" which covers similar topics in more depth.
If you're interested in getting updated each time I publish a new post, feel free to sign up for my newsletter.
Keep on building awesome stuff! 🚀
driesvints, elkdev liked this article
Other articles you might like
Laravel Custom Query Builders Over Scopes
Hello 👋 Alright, let's talk about Query Scopes. They're awesome, they make queries much easier to r...
Access Laravel before and after running Pest tests
How to access the Laravel ecosystem by simulating the beforeAll and afterAll methods in a Pest test....
🍣 Sushi — Your Eloquent model driver for other data sources
In Laravel projects, we usually store data in databases, create tables, and run migrations. But not...
The Laravel portal for problem solving, knowledge sharing and community building.
The community