Laravel Mass Assignment Protection - Blacklist V.S. Whitelist
Written on
TLDR: Use whitelist instead of blacklist. Laravel will attempt to mass-assign all attributes that aren’t in the blacklist, including properties/columns that aren’t on the table, causing SQL error.
In Laravel (At least on version 4.2.6), there is a convenient method to insert data into the database.
<?php
// ...
$user = new User(Input::all());
So, the line of code above will take all of the form data and assign them to the User
model’s attributes. This means that if we don’t put in any safe guard, anyone could modify the request parameters and set any value for any of the property. This could includes things like user’s roles and permissions, ids, or any other sensitive data. Of course, we could just assign the attributes one-by-one, but that would make our code a lot more verbose.
Fortunately, Laravel provide two easy ways to safeguard against this kind of mass assignment vulnerability. Either specify a list of fields that can be mass assigned (whitelist), or specify a list of fields that can’t be mass assigned (blacklist). While it seems like the two do the same thing, there are some subtle differences that could cause a bit of confusion.
For example, we got this table of User:
<?php
// ...
Schema::create('User', function($table)
{
$table->increments('id');
$table->string('name');
$table->string('email');
$table->unsignedInteger('role');
});
Say we want to protect the role
field from being mass-assignment, so, there are two ways to do this in the User
model. We can either specify a guarded
property to specify a list of fields that we want to exclude from mass-assignment:
<?php
// ...
class User extends Eloquent{
public $timestamps = false;
protected $guarded = ['role'];
}
Or we can specify a fillable
property to specify a list of fields that we want to allow for mass-assignment:
<?php
// ...
class User extends Eloquent{
public $timestamps = false;
protected $fillable = ['name', 'email'];
}
While you would most probably wouldn’t notice any difference, but let’s take a look at the Laravel’s Model.php
source code. First, the code that perform the mass assignment operation.
<?php
// ...
public function fill(array $attributes)
{
$totallyGuarded = $this->totallyGuarded();
foreach ($this->fillableFromArray($attributes) as $key => $value)
{
$key = $this->removeTableFromKey($key);
if ($this->isFillable($key))
{
$this->setAttribute($key, $value);
}
elseif ($totallyGuarded)
{
throw new MassAssignmentException($key);
}
}
return $this;
}
We can see that the code loop through the list fillable attributes via the fillableFromArray
method, so let’s take a look at it.
<?php
// ...
protected function fillableFromArray(array $attributes)
{
if (count($this->fillable) > 0 && ! static::$unguarded)
{
return array_intersect_key($attributes, array_flip($this->fillable));
}
return $attributes;
}
The method check if we have any value in the fillable
property, and if so, return a list of properties inside the attributes
variable that intersect with the fillable
array, else simply return the attributes
itself. This means if we have a fillable
property defined for our model, the mass assignment will not process that attributes that aren’t specified in the fillable
property.
So, let’s move on to how Laravel guards the attributes in the guarded
property. Let’s go back to the fill
method. We can see that for every attributes, the method will check if the key is fillable by calling the isFillable
method.
<?php
// ...
public function isFillable($key)
{
if (static::$unguarded) return true;
if (in_array($key, $this->fillable)) return true;
if ($this->isGuarded($key)) return false;
return empty($this->fillable) && ! starts_with($key, '_');
}
public function isGuarded($key)
{
return in_array($key, $this->guarded) || $this->guarded == array('*');
}
if the key is not listed in the fillable
property, it will call another method isGuarded
, that checks if the key is specified in the guarded
property.
This means, if fillable
is not specified, and the key is not listed in the guarded
property, the framework will assume that the key can be safely mass-assigned. Sound perfectly fine and should be expected, but when we include a property that is not a field of the table, it will cause an error, because the framework will try to insert a data into a column that doesn’t exists. The same problem will not occur if you use a while list since the framework will only process the keys that are in the array.
In conclusion, the best way to protect against mass-assignment vulnerability is to use the whitelist instead of the blacklist. From a security standpoint, it is better to explicitly specify the things that you want to allow anyway, and it also wouldn’t make Laravel try and insert the a non-existing field into the database.