Under the Hood

Model::create() - Mass Assignment Protection Deep Dive

Learn how Laravel's Model::create() works with mass assignment protection. Understand fillable, guarded, and the security mechanisms that protect your database in Laravel 12.

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

|
12-Dec-2025
8 min read
Model::create() - Mass Assignment Protection Deep Dive

The Model::create() method seems innocent—pass an array, get a new database record. But this convenience hides a critical security feature: mass assignment protection. Without it, malicious users could modify any database column. Let's explore how Laravel protects your data while keeping your code clean.

What Is Model::create()?

The create() method creates and saves a new model in a single step:

// Create a new user
$user = User::create([
    'name' => 'John Doe',
    'email' => 'john@example.com',
    'password' => Hash::make('secret'),
]);

// Combines these two operations:
$user = new User();
$user->name = 'John Doe';
$user->email = 'john@example.com';
$user->password = Hash::make('secret');
$user->save();

Convenient syntax, but powerful security underneath.

The Mass Assignment Vulnerability

Without protection, create() would be dangerous:

// Controller receiving user input
public function store(Request $request)
{
    // Dangerous - no protection
    User::create($request->all());
}

A malicious user could send:

{
    "name": "Hacker",
    "email": "hacker@evil.com",
    "password": "secret",
    "is_admin": true,
    "account_balance": 1000000
}

Without mass assignment protection, they'd create an admin account with a million-dollar balance. Laravel prevents this.

The create() Flow

When you call Model::create(), Laravel executes a security-conscious process:

Step 1: Static Method Entry

// When you call
$user = User::create($attributes);

// Behind the scenes
public static function create(array $attributes = [])
{
    $model = new static($attributes);
    $model->save();
    
    return $model;
}

The static method creates a new instance and saves it.

Step 2: Constructor Mass Assignment

// Behind the scenes in __construct
public function __construct(array $attributes = [])
{
    $this->syncOriginal();
    
    $this->fill($attributes);
}

The constructor calls fill() to set attributes.

Step 3: The fill() Method

// Behind the scenes
public function fill(array $attributes)
{
    $totallyGuarded = $this->totallyGuarded();
    
    foreach ($this->fillableFromArray($attributes) as $key => $value) {
        if ($this->isFillable($key)) {
            $this->setAttribute($key, $value);
        } elseif ($totallyGuarded) {
            throw new MassAssignmentException($key);
        }
    }
    
    return $this;
}

This is where mass assignment protection happens.

Step 4: Fillable Check

// Behind the scenes
public function isFillable($key)
{
    // If fillable is defined, check if key is in it
    if (in_array($key, $this->getFillable())) {
        return true;
    }
    
    // If guarded contains '*', nothing is fillable
    if ($this->isGuarded($key)) {
        return false;
    }
    
    return empty($this->getFillable()) && !str_starts_with($key, '_');
}

Laravel checks if the attribute can be mass-assigned.

Step 5: Attribute Setting

// Behind the scenes
protected function setAttribute($key, $value)
{
    // Check for mutator
    if ($this->hasSetMutator($key)) {
        return $this->setMutatedAttributeValue($key, $value);
    }
    
    // Cast the value
    $value = $this->castAttributeToSave($key, $value);
    
    // Store in attributes array
    $this->attributes[$key] = $value;
}

Allowed attributes are set on the model.

Step 6: Database Insert

// Behind the scenes in save()
if (!$this->exists) {
    $saved = $this->performInsert($query);
}

// Generates SQL
INSERT INTO users (name, email, password, created_at, updated_at) 
VALUES (?, ?, ?, ?, ?)

The model is inserted into the database.

The $fillable Property

Define which attributes can be mass-assigned:

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

// These work
User::create([
    'name' => 'John',
    'email' => 'john@example.com',
    'password' => Hash::make('secret'),
]);

// This is silently ignored (is_admin not fillable)
User::create([
    'name' => 'John',
    'email' => 'john@example.com',
    'is_admin' => true, // Ignored!
]);

Whitelist approach: Only explicitly listed attributes can be mass-assigned.

The $guarded Property

Define which attributes CANNOT be mass-assigned:

class User extends Model
{
    protected $guarded = [
        'is_admin',
        'account_balance',
    ];
}

// These work
User::create([
    'name' => 'John',
    'email' => 'john@example.com',
]);

// These are silently ignored
User::create([
    'is_admin' => true,        // Guarded!
    'account_balance' => 1000, // Guarded!
]);

Blacklist approach: Everything except listed attributes can be mass-assigned.

$fillable vs $guarded

Use $fillable (Recommended)

protected $fillable = [
    'name',
    'email',
    'password',
];

Pros:

  • Explicit about what's allowed
  • Safer default (deny everything except listed)
  • New columns are protected by default

Cons:

  • Must update when adding safe columns

Use $guarded

protected $guarded = [
    'id',
    'is_admin',
];

// Or guard everything
protected $guarded = ['*'];

Pros:

  • Less maintenance for models with many columns
  • New columns are allowed by default

Cons:

  • Less secure (new columns allowed by default)
  • Easy to forget to guard sensitive columns

The Golden Rule

Never use both simultaneously. Choose one approach:

// Good - uses fillable
protected $fillable = ['name', 'email'];

// Good - uses guarded
protected $guarded = ['is_admin'];

// Bad - confusing and error-prone
protected $fillable = ['name', 'email'];
protected $guarded = ['is_admin'];

Bypassing Mass Assignment Protection

Sometimes you need to bypass protection:

forceFill() Method

// Ignores fillable/guarded
User::create([
    'name' => 'Admin',
    'email' => 'admin@example.com',
])->forceFill([
    'is_admin' => true,
])->save();

Use forceFill() for internal operations, never with user input.

Guarding Nothing

protected $guarded = [];

// Everything is mass-assignable
User::create($request->all()); // Dangerous!

Warning: Only use $guarded = [] if you validate ALL input thoroughly.

Unguard Temporarily

// Disable mass assignment protection
Model::unguard();

User::create(['is_admin' => true]); // Works

// Re-enable protection
Model::reguard();

Useful for seeding, never for production code with user input.

Best Practices for Mass Assignment

1. Always Validate Before create()

// Good - validated input
public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users',
        'password' => 'required|min:8',
    ]);
    
    User::create($validated);
}

// Bad - unvalidated input
public function store(Request $request)
{
    User::create($request->all()); // Dangerous!
}

2. Use $fillable for Security-Critical Models

class User extends Model
{
    // Explicit whitelist
    protected $fillable = [
        'name',
        'email',
        'password',
    ];
    
    // is_admin, account_balance, etc. are protected
}

3. Hash Passwords in Mutators, Not Controllers

class User extends Model
{
    protected $fillable = ['name', 'email', 'password'];
    
    // Automatic password hashing
    public function setPasswordAttribute($value)
    {
        $this->attributes['password'] = Hash::make($value);
    }
}

// Controller stays clean
User::create([
    'name' => 'John',
    'email' => 'john@example.com',
    'password' => 'plain-text', // Automatically hashed
]);

4. Never Use request()->all() Directly

// Bad - passes ALL input including hidden fields
User::create($request->all());

// Good - only specific fields
User::create($request->only(['name', 'email', 'password']));

// Better - validated input
User::create($request->validated());

Common Patterns

Create with Relationships

$user = User::create([
    'name' => 'John Doe',
    'email' => 'john@example.com',
]);

// Create related records
$user->posts()->create([
    'title' => 'First Post',
    'content' => 'Hello World',
]);

Create or Update (Upsert)

// Update if exists, create if not
User::updateOrCreate(
    ['email' => 'john@example.com'], // Search criteria
    ['name' => 'John Doe']            // Values to set
);

Batch Creation

// Create multiple records
User::insert([
    ['name' => 'User 1', 'email' => 'user1@example.com'],
    ['name' => 'User 2', 'email' => 'user2@example.com'],
]);

// Note: insert() bypasses mass assignment protection
// And doesn't fire model events

firstOrCreate

// Find or create
$user = User::firstOrCreate(
    ['email' => 'john@example.com'],
    ['name' => 'John Doe']
);

Model Events with create()

The create() method fires several events:

class User extends Model
{
    protected static function booted()
    {
        static::creating(function ($user) {
            // Before INSERT query
            Log::info('Creating user', ['email' => $user->email]);
        });
        
        static::created(function ($user) {
            // After INSERT query
            Mail::to($user)->send(new WelcomeEmail());
        });
    }
}

Events fire in this order:

  1. creating - Before database insert
  2. created - After database insert
  3. saved - After any save operation

Error Handling

try {
    $user = User::create([
        'name' => 'John',
        'email' => 'duplicate@example.com', // Duplicate email
    ]);
} catch (\Illuminate\Database\QueryException $e) {
    // Handle database errors (unique constraint, etc.)
    if ($e->getCode() === '23000') {
        return back()->withErrors(['email' => 'Email already exists']);
    }
}

Testing create() with Mass Assignment

public function test_cannot_mass_assign_admin_role()
{
    $user = User::create([
        'name' => 'Test User',
        'email' => 'test@example.com',
        'is_admin' => true, // Attempt to exploit
    ]);
    
    $this->assertFalse($user->is_admin);
    $this->assertDatabaseHas('users', [
        'email' => 'test@example.com',
        'is_admin' => false,
    ]);
}

Performance Considerations

// create() - Fires events, runs through full model lifecycle
User::create(['name' => 'John']); // ~10-20ms

// insert() - Raw query, no events, no mass assignment protection
User::insert(['name' => 'John']); // ~5-10ms

// Use create() for normal operations
// Use insert() only for bulk operations where you control the data

Debugging Mass Assignment Issues

// Enable mass assignment exceptions
// In config/app.php
'debug' => true,

// Or in code
protected $guarded = ['*']; // Guard everything

User::create(['name' => 'John']); 
// Throws: MassAssignmentException

Common Mistakes

Mistake 1: Forgetting to Define $fillable

class Post extends Model
{
    // No $fillable or $guarded defined
}

Post::create(['title' => 'Test']); // Silently fails!

// Fix: Define $fillable
protected $fillable = ['title', 'content'];

Mistake 2: Using Both $fillable and $guarded

// Confusing - don't do this
protected $fillable = ['name', 'email'];
protected $guarded = ['is_admin'];

// Pick one approach
protected $fillable = ['name', 'email'];

Mistake 3: Allowing All with Empty Guarded

// Dangerous in production
protected $guarded = [];

User::create($request->all()); // All input allowed!

Conclusion

Laravel's Model::create() method brilliantly balances convenience and security through mass assignment protection. By understanding how $fillable and $guarded work, the flow from user input to database insert, and best practices for validation, you can write code that's both clean and secure.

Remember: mass assignment protection is your first line of defense against malicious input. Always define $fillable, validate user input, and never blindly trust request()->all().


Building secure Laravel applications with complex data models? At NeedLaravelSite, we specialize in Laravel security and application migrations from version 7 to 12. From mass assignment protection to input validation strategies, we build applications that are secure by default.


Article Tags

laravel model create mass assignment laravel fillable vs guarded mass assignment best practices laravel mass assignment security

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