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
$fillableover$guardedby 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.