Mass Assignment Vulnerabilities Analyzer
| Analyzer ID | Category | Severity | Time To Fix |
|---|---|---|---|
mass-assignment-vulnerabilities | 🛡️ Security | High | 25 minutes |
What This Checks
Detects mass assignment vulnerabilities where unfiltered user input is passed to Eloquent models or query builders, allowing attackers to modify unintended database fields. This analyzer checks for:
- Models without
$fillableor$guardedprotection - Dangerous method calls with
request()->all() - Query builder operations with raw request data
- Multiple unsafe request data retrieval patterns
- Missing
$hiddenattributes for sensitive fields (password, api_token, remember_token, etc.) - Relationship operations with unfiltered data (
sync(),attach(),updateExistingPivot(), etc.) - Nested mass assignment via dot notation patterns like
$request->input('user.profile') - Blacklist filtering warnings when using
request()->except()instead of the saferonly()approach
Why It Matters
Mass assignment is one of the most dangerous security vulnerabilities in Laravel applications because it allows attackers to:
- Privilege Escalation: Set
is_admin = 1to gain administrative access - Account Takeover: Modify email addresses or passwords of other users
- Data Manipulation: Update protected fields like prices, status, or verification timestamps
- Bypass Business Logic: Circumvent validation rules and access controls
Real-World Impact: GitHub 2012
In March 2012, GitHub suffered a mass assignment vulnerability that allowed attackers to add SSH keys to any repository, including Rails's own codebase. This single vulnerability led to:
- Complete system compromise requiring emergency shutdown
- Forced suspension of GitHub services for security audit
- Major architectural security overhaul
The attack exploited unprotected mass assignment by sending:
POST /repositories
{
"public_key[id]": "attacker_key_id"
}Modern Laravel applications face identical risks when using request()->all() without proper model protection.
How to Fix
Quick Fix (5 minutes)
Add mass assignment protection to all Eloquent models immediately:
Before (❌):
class User extends Model
{
// No protection - vulnerable to mass assignment
}After (✅):
class User extends Model
{
// Whitelist approach (recommended)
protected $fillable = ['name', 'email', 'bio', 'avatar'];
// OR blacklist approach
protected $guarded = ['id', 'is_admin', 'role', 'created_at', 'updated_at'];
}Controller update:
Before (❌):
public function update(Request $request, User $user)
{
$user->update($request->all()); // Dangerous
}After (✅):
public function update(Request $request, User $user)
{
$user->update($request->only(['name', 'email', 'bio'])); // Safe
}Proper Fix (25 minutes)
Step 1: Define model protection strategy
Choose between $fillable (whitelist - recommended) or $guarded (blacklist):
// RECOMMENDED: Whitelist approach
class User extends Model
{
protected $fillable = [
'name',
'email',
'bio',
'avatar',
];
}
// ALTERNATIVE: Blacklist approach
class User extends Model
{
protected $guarded = [
'id',
'is_admin',
'role',
'email_verified_at',
'remember_token',
'created_at',
'updated_at',
];
}Best Practice: Use $fillable for better security - new fields are protected by default.
Step 2: Create Form Request validation
Generate validated request class:
php artisan make:request UpdateUserRequestBefore (❌):
public function update(Request $request, User $user)
{
$user->update($request->all());
return $user;
}After (✅):
// app/Http/Requests/UpdateUserRequest.php
class UpdateUserRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->id === $this->route('user')->id;
}
public function rules(): array
{
return [
'name' => 'sometimes|string|max:255',
'email' => 'sometimes|email|unique:users,email,'.$this->user()->id,
'bio' => 'nullable|string|max:1000',
'avatar' => 'nullable|url',
];
}
}
// Controller
public function update(UpdateUserRequest $request, User $user)
{
$user->update($request->validated()); // Only validated fields
return $user;
}Step 3: Fix all dangerous patterns
Pattern 1: Eloquent create/update with request()->all()
Before (❌):
User::create($request->all());
Product::create($request->input());
$user->update($request->post());
$user->fill($request->get())->save();After (✅):
User::create($request->validated());
Product::create($request->only(['name', 'description', 'price']));
$user->update($request->validated());
$user->fill($request->validated())->save();Pattern 2: Query Builder operations
Before (❌):
DB::table('users')->update($request->all());
DB::table('products')->insert($request->all());After (✅):
DB::table('users')->update([
'name' => $request->validated('name'),
'email' => $request->validated('email'),
'updated_at' => now(),
]);
DB::table('products')->insert([
'name' => $request->validated('name'),
'price' => $request->validated('price'),
'created_at' => now(),
]);Pattern 3: Empty $guarded array
Before (❌):
class Product extends Model
{
protected $guarded = []; // Allows mass assignment of ALL fields
}After (✅):
class Product extends Model
{
protected $fillable = ['name', 'description', 'price', 'stock'];
}Pattern 4: Missing $hidden for sensitive data
Before (❌):
class User extends Model
{
protected $fillable = ['name', 'email', 'password'];
// Sensitive fields exposed in JSON/array output
}After (✅):
class User extends Model
{
protected $fillable = ['name', 'email', 'password'];
// Hide sensitive fields from serialization
protected $hidden = [
'password',
'remember_token',
'api_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
}Pattern 5: Relationship operations with unfiltered data
Before (❌):
public function updateRoles(Request $request, User $user)
{
$user->roles()->sync($request->input('roles')); // Dangerous
$user->permissions()->attach($request->all()); // Very dangerous
}After (✅):
public function updateRoles(UpdateRolesRequest $request, User $user)
{
// Validate role IDs exist and user is authorized to assign them
$validRoleIds = Role::whereIn('id', $request->input('roles', []))
->where('assignable', true)
->pluck('id')
->toArray();
$user->roles()->sync($validRoleIds);
}
// Or use validated data with explicit allowed values
public function updatePermissions(Request $request, User $user)
{
$validated = $request->validate([
'permissions' => 'array',
'permissions.*' => 'exists:permissions,id',
]);
$user->permissions()->sync($validated['permissions'] ?? []);
}Pattern 6: Blacklist filtering (except) vs Whitelist (only)
Before (❌):
// Blacklist approach - easy to forget new sensitive fields
$user->update($request->except(['is_admin', 'role']));
// New fields added later may be vulnerable
// e.g., 'api_token', 'email_verified_at' could be mass-assignedAfter (✅):
// Whitelist approach - only explicitly allowed fields
$user->update($request->only(['name', 'email', 'bio', 'avatar']));
// Or best: use validated data from Form Request
$user->update($request->validated());Why whitelist is safer: When new fields are added to your model, the blacklist approach requires you to remember to add them to except(). With whitelist (only() or validated()), new fields are automatically protected.
Pattern 7: Nested mass assignment
Before (❌):
// Dot notation can bypass simple $fillable checks
$user->update($request->input('user')); // Gets nested array
$user->profile()->update($request->input('user.profile'));After (✅):
// Explicitly validate and extract nested data
$validated = $request->validate([
'user.name' => 'required|string|max:255',
'user.email' => 'required|email',
'user.profile.bio' => 'nullable|string|max:1000',
]);
$user->update([
'name' => $validated['user']['name'],
'email' => $validated['user']['email'],
]);
$user->profile()->update([
'bio' => $validated['user']['profile']['bio'] ?? null,
]);Step 4: Test your implementation
Create automated tests:
// tests/Feature/MassAssignmentTest.php
public function test_cannot_mass_assign_admin_role()
{
$response = $this->postJson('/api/users', [
'name' => 'John Doe',
'email' => 'john@example.com',
'is_admin' => 1, // Attempt to set admin
]);
$user = User::where('email', 'john@example.com')->first();
$this->assertFalse($user->is_admin); // Should NOT be admin
}Manual testing with curl:
# Attempt malicious request
curl -X POST http://localhost/api/users \
-H "Content-Type: application/json" \
-d '{"name":"John","email":"john@test.com","is_admin":1,"role":"admin"}'
# Verify in database
php artisan tinker
>>> User::latest()->first()
>>> # is_admin and role should NOT be setReferences
- Eloquent: Mass Assignment
- Validation: Form Request Validation
- OWASP: Mass Assignment Cheat Sheet
- CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
- GitHub Mass Assignment Incident (2012)
- Mass Assignment: A New Software Vulnerability (OWASP PDF)
Related Analyzers
- SQL Injection Analyzer - Detects raw SQL queries with user input
- XSS Vulnerabilities Analyzer - Prevents cross-site scripting attacks
- CSRF Protection Analyzer - Validates CSRF token protection
- Authentication & Authorization Analyzer - Validates policy and gate checks
- Fillable Foreign Key Analyzer - Detects foreign keys in fillable arrays