Every Laravel model faces a critical security decision: use $fillable or $guarded? This choice determines which database columns users can modify through mass assignment. Choose wrong, and you expose your application to data manipulation attacks. Let's explore both approaches and learn which one protects your data best.
The Security Problem
Without protection, malicious users could modify any database column:
// Dangerous - no protection
class User extends Model
{
// No $fillable or $guarded defined
}
// Controller
User::create($request->all());
// Attacker sends:
{
"name": "Hacker",
"email": "hack@evil.com",
"is_admin": true, // ← Exploited!
"account_balance": 999999 // ← Exploited!
}
The attacker just created an admin account with unlimited balance. Laravel prevents this through $fillable and $guarded.
The $fillable Property - Whitelist Approach
$fillable explicitly lists which attributes CAN be mass-assigned:
class User extends Model
{
protected $fillable = [
'name',
'email',
'password',
];
}
// Only name, email, password can be mass-assigned
User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => Hash::make('secret'),
'is_admin' => true, // ← Silently ignored
]);
Default behavior: Deny everything except what's listed.
How $fillable Works
// Behind the scenes
public function isFillable($key)
{
// Check if key is in fillable array
if (in_array($key, $this->getFillable())) {
return true; // Allow
}
return false; // Deny
}
Laravel checks each attribute against the $fillable array. Not listed? Not allowed.
$fillable Benefits
1. Explicit Security
protected $fillable = ['name', 'email'];
// Clear what's allowed
// Clear what's protected
2. Safe by Default
// Add new column to database
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_verified')->default(false);
});
// Column is automatically protected
// Must explicitly add to $fillable to allow mass assignment
3. Easy to Audit
// Review fillable to see what users can modify
protected $fillable = [
'name',
'email',
'bio',
'avatar',
];
$fillable Drawbacks
1. Maintenance Overhead
// Add 20 safe columns to database
// Must update $fillable 20 times
protected $fillable = [
'field1', 'field2', 'field3', 'field4', 'field5',
'field6', 'field7', 'field8', 'field9', 'field10',
// ... gets tedious
];
2. Easy to Forget
// Add new column
Schema::table('posts', function (Blueprint $table) {
$table->string('subtitle');
});
// Forget to add to $fillable
protected $fillable = ['title', 'content']; // Missing subtitle!
// Mass assignment silently fails
Post::create(['title' => 'Test', 'subtitle' => 'Oops']); // subtitle not saved
The $guarded Property - Blacklist Approach
$guarded explicitly lists which attributes CANNOT be mass-assigned:
class User extends Model
{
protected $guarded = [
'id',
'is_admin',
'account_balance',
];
}
// Everything except id, is_admin, account_balance can be mass-assigned
User::create([
'name' => 'John Doe', // ✓ Allowed
'email' => 'john@example.com', // ✓ Allowed
'bio' => 'Developer', // ✓ Allowed
'is_admin' => true, // ✗ Ignored
]);
Default behavior: Allow everything except what's listed.
How $guarded Works
// Behind the scenes
public function isGuarded($key)
{
// If guarded contains '*', everything is guarded
if ($this->getGuarded() === ['*']) {
return true;
}
// Check if key is in guarded array
return in_array($key, $this->getGuarded());
}
Laravel checks if the attribute is in the $guarded blacklist.
$guarded Benefits
1. Less Maintenance
// Only guard sensitive columns
protected $guarded = ['id', 'is_admin'];
// Add 50 new safe columns - no code changes needed
// They're automatically mass-assignable
2. Fewer Updates
// Models with many safe columns
class Product extends Model
{
protected $guarded = ['id', 'vendor_id'];
// name, price, sku, description, weight, dimensions,
// color, size, stock, etc. all automatically fillable
}
3. Flexible for Internal Tools
// Admin tools with many fields
protected $guarded = ['id'];
// Most fields are safe in admin context
$guarded Drawbacks
1. Unsafe by Default
// Add new sensitive column
Schema::table('users', function (Blueprint $table) {
$table->decimal('salary', 10, 2);
});
// Forgot to add to $guarded
protected $guarded = ['id', 'is_admin']; // Missing salary!
// Now users can set their own salary!
User::create([
'name' => 'Hacker',
'salary' => 999999.99, // ← Oops!
]);
2. Less Obvious Security
// What can users modify?
// Must read entire schema and subtract guarded columns
protected $guarded = ['id', 'is_admin'];
// Not immediately clear what's allowed
3. Requires Discipline
// Easy to forget to guard new sensitive columns
// Security by remembering is risky
The Empty Array Special Case
Empty $fillable - Nothing Allowed
protected $fillable = [];
User::create(['name' => 'John']); // Nothing saved!
// Everything is protected
// Useful for read-only models
Empty $guarded - Everything Allowed
protected $guarded = [];
User::create($request->all()); // Everything saved!
// DANGEROUS unless you validate thoroughly
// Only use with strict validation
The Asterisk Guard
Guard everything:
protected $guarded = ['*'];
User::create(['name' => 'John']); // Nothing saved!
// Equivalent to empty $fillable
// Useful for models that should never use mass assignment
Decision Matrix: Which to Use?
Use $fillable When:
1. Security-Critical Models
class User extends Model
{
protected $fillable = ['name', 'email', 'password'];
// Explicit about user-modifiable fields
// Admin, balance, roles are protected
}
2. Models with Sensitive Data
class Payment extends Model
{
protected $fillable = ['description', 'notes'];
// Amount, status, transaction_id protected
}
3. Public-Facing APIs
class Post extends Model
{
protected $fillable = ['title', 'content', 'excerpt'];
// Clear what external users can modify
}
4. You Want Safe Defaults
// New columns are protected automatically
// Must consciously choose to expose them
Use $guarded When:
1. Internal Admin Models
class AdminLog extends Model
{
protected $guarded = ['id', 'created_at'];
// Most fields are safe in admin context
}
2. Models with Many Safe Columns
class Product extends Model
{
protected $guarded = ['id', 'vendor_id', 'verified_at'];
// 50+ product attributes that are all safe
}
3. Rapid Prototyping
protected $guarded = ['id'];
// Quick development, tighten later
4. You Have Strict Validation
protected $guarded = [];
// But ALWAYS validate:
$validated = $request->validate([
'name' => 'required|string',
// ... explicit validation rules
]);
The Golden Rules
Rule 1: Never Use Both
// WRONG - Confusing and error-prone
protected $fillable = ['name', 'email'];
protected $guarded = ['is_admin'];
// Pick one approach
protected $fillable = ['name', 'email'];
Rule 2: Always Define One
// WRONG - No protection
class User extends Model
{
// No $fillable or $guarded
}
// RIGHT - Choose one
protected $fillable = ['name', 'email'];
// OR
protected $guarded = ['id', 'is_admin'];
Rule 3: Validate Before Mass Assignment
// ALWAYS validate user input
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
]);
User::create($validated);
}
Real-World Scenarios
E-commerce Product
class Product extends Model
{
// Many attributes, few sensitive ones
protected $guarded = [
'id',
'vendor_id',
'commission_rate',
'verified_at',
];
}
User Authentication
class User extends Model
{
// Few safe attributes, many sensitive ones
protected $fillable = [
'name',
'email',
'password',
'bio',
'avatar',
];
}
API Token
class PersonalAccessToken extends Model
{
// Very restricted
protected $fillable = [
'name',
];
// User can only set token name
// All other fields controlled by system
}
Switching Strategies
From $guarded to $fillable
// Before - using guarded
protected $guarded = ['id', 'is_admin'];
// After - switching to fillable (more secure)
protected $fillable = [
'name',
'email',
'bio',
'avatar',
// Explicitly list all safe columns
];
From $fillable to $guarded
// Before - using fillable
protected $fillable = [
'field1', 'field2', 'field3', 'field4', 'field5',
'field6', 'field7', 'field8', 'field9', 'field10',
// Too many fields
];
// After - switching to guarded (less maintenance)
protected $guarded = [
'id',
'sensitive_field',
];
Testing Mass Assignment Protection
public function test_cannot_mass_assign_admin_flag()
{
$user = User::create([
'name' => 'Test User',
'email' => 'test@example.com',
'is_admin' => true, // Try to exploit
]);
$this->assertFalse($user->is_admin);
}
public function test_can_mass_assign_allowed_fields()
{
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
]);
$this->assertEquals('John Doe', $user->name);
$this->assertEquals('john@example.com', $user->email);
}
Best Practices
1. Choose $fillable for Security-Critical Models
// User, Payment, Order, Transaction
protected $fillable = ['specific', 'safe', 'fields'];
2. Document Your Choice
class User extends Model
{
// Using fillable for explicit security
// Only name and email can be set by users
protected $fillable = ['name', 'email'];
}
3. Regular Security Audits
// Review periodically:
// - Are new columns accidentally mass-assignable?
// - Do fillable arrays include everything needed?
// - Are guarded arrays missing new sensitive fields?
4. Use FormRequests for Validation
class StoreUserRequest extends FormRequest
{
public function rules()
{
return [
'name' => 'required|string',
'email' => 'required|email|unique:users',
];
}
}
// Controller
User::create($request->validated());
Conclusion
The choice between $fillable and $guarded fundamentally impacts your application's security. $fillable provides explicit, safe-by-default protection—ideal for security-critical models. $guarded offers convenience and flexibility—better for internal tools and models with many safe columns.
Remember: $fillable is safer, $guarded is easier. When in doubt, choose $fillable. Security should never be sacrificed for convenience.
Need help securing your Laravel models or performing security audits? At NeedLaravelSite, we specialize in Laravel security and application migrations from version 7 to 12. From mass assignment protection to comprehensive security reviews, we ensure your data stays protected.