Use $fillable in Laravel. It is an allow list, so a column the request can set must be named on purpose. $guarded is a block list, and an empty $guarded = [] makes every column mass assignable, including flags like is_admin that you never meant to expose.
What the two properties do
Mass assignment happens when you hand request data to create, fill or update in one call. The Laravel documentation describes the risk plainly: a user sends an unexpected field, such as is_admin, and it changes a column you did not intend. Eloquent models are protected by default, and you must declare either $fillable or $guarded before create will work.
| Property | Meaning | Default posture | Risk when you add a column |
|---|---|---|---|
$fillable | Only these attributes may be mass assigned | Closed | New column stays protected until you list it |
$guarded | These attributes may not be mass assigned | Open for everything else | New column is writable unless you remember to guard it |
$guarded = [] | Nothing is guarded | Fully open | Every column is writable |
Why the allow list ages better
A block list is only as good as your memory. Six months after a model is written, someone adds role, balance or email_verified_at. With $guarded = ['id'] that column is writable from the first deploy. With $fillable it stays locked until a reviewer sees the new line in a diff.
The documentation says that if you unguard a model you should hand-craft the arrays you pass to fill, create and update. That is a workable rule for a small internal tool. It is a poor rule for a codebase with many contributors.
Steps to tighten a model
- List the columns a form or API endpoint should set, and nothing else.
- Put them in
$fillable. Keep ownership, role, status, price and verification columns out. - Build the input array from validated data, not from the whole request.
- Turn on strict behaviour in development, shown below.
- Search the codebase for
guarded = []and forforceFill, which skips the protection on purpose.
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}
By default, attributes missing from $fillable are silently discarded. That is fine in production, but in development it hides mistakes. The call above makes Eloquent throw an exception when code tries to fill a column that is not allowed, so you notice early.
Two details are worth knowing. For JSON columns, each key you want writable must be listed in $fillable, for example options->enabled. And Laravel does not support updating nested JSON attributes when you use $guarded.
Try it yourself
Open the walkthrough below to see how a code scan reports a finding. Click through a result and look at how the flagged line is tied to its source.
Click through it at your own pace, or open it full screen.
Quick check
Is guarded = [] ever acceptable?
It can be reasonable in a seeder or a one-off script where no request data reaches the model. In a controller path, prefer $fillable and validated input.
Does validation replace fillable?
No. Validation checks the values you expect. $fillable limits which columns can be set at all. Use both, and pass only validated data.
Does fillable protect me if I call forceFill?
No. forceFill bypasses the protection, so treat each use as something to review.
Safeguard offers dependency scanning, SBOMs and reachability analysis, with policy gates on pull requests. You can try a free scan at gold.safeguard.sh/scan.
Sources
Last reviewed: 19 October 2026.