How to Protect Laravel Applications from Mass Assignment Vulnerabilities
When starting out with Laravel or working on database seeders and test suites, you have almost certainly encountered the MassAssignmentException error. To bypass mass assignment restrictions quickly, many developers reach for a shortcut: setting protected $guarded = []; across all models or calling Model::unguard() globally inside their AppServiceProvider.
While unguarding models might seem convenient and eliminate repetitive $fillable arrays, leaving mass assignment unprotected in production is one of the most common and dangerous security vulnerabilities in web applications.
In this tutorial, we will understand how mass assignment vulnerabilities work, why using Model::unguard() or an empty $guarded array can compromise your application, and how to properly secure your Eloquent models and controller inputs.
What is Eloquent Mass Assignment?
Mass assignment occurs when you pass an array of attributes directly into an Eloquent model method—such as create(), update(), fill(), or updateOrCreate()—instead of setting each property manually one by one.
For example, consider this common controller pattern:
public function update(Request $request, User $user)
{
// Mass assigning the entire incoming HTTP payload
$user->update($request->all());
return response()->json(['message' => 'Profile updated successfully!']);
}
By default, Laravel protects you against unauthorized attribute updates by requiring you to define either a $fillable (whitelist) or $guarded (blacklist) array on the model class.
Why Model::unguard() is Dangerous: A Real-World Attack Scenario
When you call Model::unguard() or declare protected $guarded = []; on your model, you are disabling Laravel's mass assignment protection entirely. Any field sent in the HTTP request that matches a database table column will be written directly to the database.
Let's understand how an attacker can exploit this.
The Vulnerable Code
Suppose you have an AppServiceProvider where models are globally unguarded:
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// DANGEROUS: Disables mass assignment protection globally
Model::unguard();
}
}
Now, look at your profile edit controller:
public function updateProfile(Request $request)
{
$user = auth()->user();
// The developer expects only 'name' and 'bio'
$user->update($request->all());
return back()->with('status', 'Profile saved!');
}
The Exploitation
A legitimate user submits a standard form with name and bio. However, an attacker inspects the request in Postman or DevTools and injects extra payload parameters:
{
"name": "Attacker Name",
"bio": "Security researcher",
"is_admin": true,
"role": "super-admin",
"email_verified_at": "2026-08-25 12:00:00",
"balance": 999999
}
Because the model is unguarded and the controller blindly passes $request->all(), Eloquent executes:
UPDATE `users` SET
`name` = 'Attacker Name',
`bio` = 'Security researcher',
`is_admin` = 1,
`role` = 'super-admin',
`email_verified_at` = '2026-08-25 12:00:00',
`balance` = 999999
WHERE `id` = 12;
Without writing any SQL injection, the attacker has escalated their privileges to an administrator and manipulated account balances.
How to Properly Protect Your Models and Inputs
Securing your application requires defense-in-depth: combining strict model-level definitions with validated controller inputs.
1. Always Use the $fillable Whitelist
The safest practice at the model level is defining explicit $fillable arrays. Only attributes listed here can ever be written via mass assignment:
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'bio',
];
}
Even if an attacker injects is_admin = true into the request, Eloquent silently ignores it because is_admin is not in $fillable.
2. Never Pass $request->all() to Models
Regardless of model configuration, never pass raw unvalidated request payloads directly to your database queries. Always use $request->validated() from Form Request classes or $request->only():
namespace App\Http\Controllers;
use App\Http\Requests\UpdateUserProfileRequest;
class UserProfileController extends Controller
{
public function update(UpdateUserProfileRequest $request)
{
$user = auth()->user();
// Safe: Only attributes validated in UpdateUserProfileRequest are passed
$user->update($request->validated());
return response()->json(['message' => 'Profile updated successfully!']);
}
}
When Is Unguarding Actually Acceptable?
Unguarding is not inherently evil—it has valid use cases when you have 100% control over the input source and no user input is involved.
1. Database Seeders and Fixtures
When seeding your local or staging database with test data, unguarding saves you from updating $fillable properties on internal tables:
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
// Safely unguard for the duration of seeding
User::unguard();
User::create([
'name' => 'System Admin',
'email' => 'admin@example.com',
'password' => bcrypt('password'),
'is_admin' => true, // Protected column allowed during seeding
]);
User::reguard();
}
}
2. The Model::shouldBeStrict() Method (Laravel Best Practice)
In modern Laravel applications, you can enable strict mode during local development. This ensures you catch missing $fillable attributes early in development without exposing yourself in production:
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Enforces strict mode (including mass assignment prevention) only in local development
Model::shouldBeStrict(! $this->app->isProduction());
}
}
When Model::shouldBeStrict() is enabled, Laravel will throw an exception during local testing if you attempt to mass assign an unfillable attribute, while failing gracefully in production.
Summary: Best Practices Checklist
- 🎯 Always specify
$fillable: Explicitly whitelist the attributes that users are allowed to modify.
- 🚫 Avoid
$guarded = []in production: Do not use empty guarded arrays to bypass mass assignment checks.
- 🔒 Validate every request: Use Laravel Form Requests or
$request->validate()and only pass$request->validated()to model methods.
- ⚡ Use
reguard()if you unguard: If you temporarily unguard a model in seeders or background scripts, always callModel::reguard()afterward.
Conclusion
Laravel's mass assignment protection exists for a critical reason: to safeguard your application from privilege escalation and unauthorized database tampering. While calling Model::unguard() might save a few keystrokes, building the habit of using explicit $fillable arrays and validated form requests ensures your applications remain secure and maintainable.
Thank you for reading this article 😊
For any query, do not hesitate to comment 💬
.png)