Under the Hood

$fillable vs $guarded - How Laravel Protects Your Data

Understand the difference between $fillable and $guarded in Laravel. Learn which mass assignment protection strategy is best for your models and security requirements in Laravel 12.

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

|
12-Dec-2025
7 min read
$fillable vs $guarded - How Laravel Protects Your Data

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.


Article Tags

fillable vs guarded eloquent fillable vs guarded fillable guarded difference best practice fillable guarded laravel protect laravel models from mass assignment

About the Author

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

Founder & CEO at CentoSquare | Creator of NeedLaravelSite | Helping Businesses Grow with Cutting-Edge Web, Mobile & Marketing Solutions | Building Innovative Products