In Depth: Mass-Assignment Vulnerabilities

[InDepth#15] There is a false confidence about mass-assignment vulnerabilities that hides how easy it is for them to occur and be exploited...

In Depth: Mass-Assignment Vulnerabilities
ℹ️
This is part of my series on the Top 10 Security Issues discovered during my Laravel Security Audits, as of April 2023.

#1 → Exposed API Keys & Passwords
#2 → Missing Authorisation
#3 → Missing Content Security Policy (CSP)
#4 → Missing Security Headers
#5 → Insecure Function Use
#6 → Outdated & Vulnerable Dependencies
#7 → Cross-Site Scripting (XSS)
#8 → Insufficient Rate Limiting
#9 → Missing Subresource Integrity (SRI)
#10 → Insufficient Input Validation & Mass-Assignment Vulnerabilities

When you think of Mass-Assignment vulnerabilities on Laravel, I guarantee you’ll immediately think of the humble $fillable Eloquent property. That’s definitely what the Laravel Docs teach you

If you’re not familiar with the $fillable Eloquent property, this is how it works:

First, you define the property on your model:

class User extends Model
{
    protected $fillable = ['name', 'email'];
}

Next, when creating or updating the model, only the allowed attributes can be filled.

$input = [
    'name'  => 'Bilbo Baggins',
    'email' => 'bilbo@example.com',
    'admin' => true
];

$user = User::create($input);

// $user->name  -> 'Bilbo Baggins'
// $user->email -> 'bilbo@example.com'
// $user->admin -> false

In the above example, the admin = true flag is ignored when creating the User model, protecting against a mass-assignment exploit to escalate the privileges of the newly created user.

And this is where many people stop learning about mass-assignment vulnerabilities and assume they are covered.

But are they?

I would argue that $fillable is a less important, and possibly even optional, protection against mass-assignment. It's great to use if you can, but not essential, and definitely not my first choice when protecting against Mass-Assignment.

In fact, I often allow mass-assignment across all fields of a Model using $guarded = [];, or even Model::unguard() across the entire app in AppServiceProvider. As controversial as that may be, I find $fillable gets in my way more often than it protects me. Disabling the protection actually makes the code cleaner and more maintainable.

So what do I do instead?