paulund
#laravel #security #eloquent

Mass Assignment Guarding

Mass assignment is a feature that lets you populate a model's attributes from an array in one step. It is convenient, but it becomes a security vulnerability if you pass user-controlled input directly without specifying which fields are allowed to be filled.

The Vulnerability

Imagine a users table with an is_admin column. Without guarding, this request would silently promote the user to admin:

POST /users
{
    "name": "Alice",
    "email": "[email protected]",
    "is_admin": true
}
// Dangerous — never do this
User::create($request->all());

Using $fillable

The $fillable property is an allowlist. Only the fields listed here can be mass-assigned:

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

Any field not in $fillable is silently ignored when using create() or fill(). The is_admin field above would be discarded.

Using $guarded

The $guarded property is a denylist — it specifies which fields cannot be mass-assigned. Everything else is allowed:

class User extends Model
{
    protected $guarded = [
        'is_admin',
        'role',
    ];
}

An empty $guarded array ($guarded = []) disables all guarding. Avoid this in production.

$fillable vs $guarded

Prefer $fillable. It forces you to consciously opt in to each field, which is the safer default. $guarded requires you to remember every sensitive field that must be excluded — easy to miss when adding new columns.

Using $request->validated() in Controllers

The model's $fillable property is a last line of defence. The better practice is to validate input first and only pass validated data to the model:

public function store(StoreUserRequest $request): RedirectResponse
{
    // validated() returns only the fields that passed your rules
    User::create($request->validated());

    return redirect()->route('users.index');
}

A Form Request defines exactly which fields are expected. Combined with $fillable, this gives you two independent layers of protection.

Checking for Vulnerabilities

If you set $guarded = [] and call create($request->all()), PHPStan with Larastan will not warn you — it is valid PHP. Instead, rely on code review and the principle of always using $request->validated() over $request->all().

Tips

  • Use $fillable over $guarded by default.
  • Always call $request->validated() in controllers, not $request->all() or $request->input().
  • Review your models when adding new sensitive columns — if the column should never be user-writable, check that it is not in $fillable.
  • In tests, use factories to create test data rather than calling create() with raw arrays from user input.