3 min read
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.