Blog / Laravel
Laravel · 5 min read · May 12, 2026

Mass assignment, explained for non-security folks

Golden padlock resting on a keyboard

Mass assignment is the vulnerability your security-minded friends keep mentioning when you show them AI-generated Laravel code. This article explains it without jargon: what it is, how an attacker actually uses it, why your AI assistant almost certainly wrote some, and the three-line fix. No security background needed — if you can read a Laravel controller, you can follow this.

The paper-form analogy

Imagine a bank where you update your details by filling in a paper form — name, address, phone — and a clerk copies your answers into the bank's ledger. Now imagine the clerk's actual rule is: copy every field the customer wrote, whatever it is, into the matching column of the ledger.

So you take the form, draw an extra box on it labeled "Account balance", write "$1,000,000", and hand it in. The clerk finds the matching column and copies it over. Nobody designed a balance field into the form — but the rule was never "copy the fields we asked for", it was "copy whatever arrives".

That clerk is your controller. The extra box is one line of curl. That is mass assignment.

What it looks like in Laravel

One line, found in nearly every vibe-coded app we audit:

public function update(Request $request) { $request->user()->update($request->all()); return back(); }

$request->all() means "everything the client sent" — not "the fields on my form". The browser form is just a suggestion; anyone can send any fields they like directly to your endpoint.

The attack, step by step

Say your users table has an is_admin column, and your profile form has name and email fields. The attacker doesn't open your form. They send the request themselves:

curl -X POST https://yourapp.com/profile \ -H "Cookie: laravel_session=<their own valid session>" \ -d "name=Eve" \ -d "email=eve@example.com" \ -d "is_admin=1"

The controller does update($request->all()), the array contains is_admin => 1, Eloquent writes it, and Eve reloads the page as an administrator. Total skill required: knowing that column names like is_admin, role, credits or plan probably exist — and attackers guess column names all day, because the cost of guessing is zero.

The same trick sets price on an order, user_id on someone else's record, verified_at on a fresh account, or balance on a wallet. Any column the model will accept, the request can fill.

"But my form doesn't even have that field"

This is the intuition that makes founders skip the fix, so it bears repeating: your form is not your API. Your endpoint accepts HTTP requests from anything — curl, Postman, a 10-line Python script. The browser UI you built is one well-behaved client among the hostile ones. Security decisions live on the server, per request, every time.

Why your AI assistant wrote it this way

Two reasons. First, $request->all() is everywhere in the training data — tutorials use it because it keeps examples short. Second, and more dangerous: Laravel actually ships a guard rail against this. Models refuse mass assignment of fields not listed in $fillable, throwing a MassAssignmentException. When an assistant hits that exception, the statistically popular "fix" is:

class User extends Model { protected $guarded = []; // "fixes" the error by disabling the safety }

That line means "every column is writable through mass assignment, forever". The error the assistant silenced was Laravel telling you it was protecting you. This exact pattern — guard rail trips, assistant removes guard rail — is why we treat $guarded = [] as a flag in every AI-code audit.

The fix: decide what is writable, twice

Layer one: list what the request may set, by validating and using only the validated data:

public function update(Request $request) { $data = $request->validate([ 'name' => ['required', 'string', 'max:120'], 'email' => ['required', 'email', 'max:254'], ]); $request->user()->update($data); return back(); }

Now is_admin=1 in the request simply never reaches the model — validation passes through only the keys you named. Layer two: on the model, keep an allow-list so even future sloppy code can't stuff sensitive columns:

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

When the application itself legitimately sets a protected column — say an admin action promoting a user — do it explicitly, so the privilege change is visible in code: $user->forceFill(['is_admin' => true])->save() inside an authorized admin controller. forceFill in a user-facing request path, on the other hand, is a finding.

One more switch worth flipping in development:

// AppServiceProvider::boot() Model::shouldBeStrict(! app()->isProduction());

Among other things this makes Eloquent throw when a request tries to fill a non-fillable attribute instead of silently dropping it — so mistakes surface in your tests, not in production behavior.

Check your app in five minutes

grep -rn "->all()" app/Http # every hit feeding update()/create() needs validation grep -rn "guarded = \[\]" app/Models # the disabled-safety flag grep -rn "forceFill" app # fine in admin code, a finding in request paths

Then look at your most sensitive columns (is_admin, role, plan, balance, user_id, verified_at) and ask of each: which line of code is allowed to write this, and is it the only one that can? If you want the full sweep beyond mass assignment, start with the pre-launch security checklist.

FAQ

Is $fillable on its own enough?

It stops column-stuffing, but validation still matters: $fillable doesn't check that an email is an email or cap string lengths, and it won't save you if someone adds a sensitive column to the list later "to make a feature work". Use both layers — they protect against different mistakes.

Is $request->only(['name','email']) safe?

Much safer than all(), because you name the keys. But it skips validation entirely, so pair it with rules — $request->validate() gives you the key filtering and the format checks in one step, which is why it's the pattern to standardize on.

Does this only matter for the User model?

No — any model with a column a user shouldn't control is exposed: orders with price or status, subscriptions with plan, posts with author_id, wallets with balance. User roles are just the most famous payoff.

Not sure what else slipped through?

A fixed-price audit finds the rest of the textbook flaws — each with a fix.