DEV Community

Cover image for Mass Assignment Vulnerabilities and Validation in Laravel
Ash Allen
Ash Allen

Posted on • Originally published at ashallendesign.co.uk

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 project's 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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',
    ];
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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',
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 the nullable or sometimes 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 or files 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
Enter fullscreen mode Exit fullscreen mode

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',
Enter fullscreen mode Exit fullscreen mode

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',
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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! 🚀

Top comments (0)