• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Mitigating OWASP Top 10 Risks: Finding and Patching mass assignment vulnerabilities in custom checkout models in Laravel

Mitigating OWASP Top 10 Risks: Finding and Patching mass assignment vulnerabilities in custom checkout models in Laravel

Understanding Mass Assignment Vulnerabilities in Laravel

Mass assignment vulnerabilities, a perennial OWASP Top 10 concern (often falling under A01:2021 – Broken Access Control or A03:2021 – Injection), arise when an application allows users to submit unexpected or unauthorized data that directly maps to model attributes. In frameworks like Laravel, this is particularly relevant when using Eloquent models and their `fill()` or `create()` methods without proper sanitization or whitelisting of attributes. An attacker can exploit this by sending additional parameters in an HTTP request that correspond to sensitive fields in the database, such as `is_admin`, `user_id`, or `price`, thereby altering application state or data they shouldn’t have access to.

Consider a typical scenario in a Laravel e-commerce application where a `Checkout` model is used to process orders. If this model is not properly secured, an attacker might attempt to manipulate the order details.

Identifying Vulnerable Code Patterns

The primary indicator of a potential mass assignment vulnerability lies in how Eloquent models are populated. If models are directly filled from request data without explicit control over which attributes are allowed, the system is at risk. This often manifests in controllers or service classes that look like the following:

Example of a Vulnerable Controller Method

In this example, the `Checkout` model is directly populated using all data from the incoming `Request` object. If the `Checkout` model has attributes like `discount_percentage` or `is_shipped` that should not be user-modifiable, this code is vulnerable.

<?php

namespace App\Http\Controllers;

use App\Models\Checkout;
use Illuminate\Http\Request;

class CheckoutController extends Controller
{
    public function store(Request $request)
    {
        // Vulnerable: Directly filling model with all request data
        $checkout = Checkout::create($request->all());

        // ... process checkout ...

        return response()->json($checkout);
    }
}

The `Checkout` Model (Potentially Vulnerable)

The vulnerability is amplified if the `Checkout` model itself doesn’t explicitly define its fillable attributes. By default, Eloquent models are *not* fillable, meaning you must explicitly define which attributes can be mass-assigned. However, if the `protected $fillable = [];` or `protected $guarded = [‘*’];` (which is the default if neither is set) is absent or misconfigured, it can lead to issues. A common mistake is to use `protected $guarded = [];` which allows all attributes to be mass-assigned, or to forget to define `$fillable` altogether.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Checkout extends Model
{
    // If this property is missing, or set to [], and $guarded is not set to ['*'],
    // then all attributes are guarded by default.
    // If $guarded = []; is used, then all attributes are fillable.
    // If $guarded = ['*']; is used, then no attributes are fillable by default.
    // The most secure approach is to use $fillable.

    // Example of a potentially insecure configuration if not managed carefully:
    // protected $guarded = []; // Allows mass assignment of ALL attributes

    // A better, but still potentially vulnerable if not exhaustive:
    // protected $fillable = [
    //     'user_id',
    //     'product_id',
    //     'quantity',
    //     'shipping_address',
    //     'billing_address',
    //     'payment_method',
    // ];

    // ... other model properties and methods ...
}

Exploitation Scenario

An attacker, knowing the structure of the `Checkout` model and the controller’s logic, could craft a malicious request. Suppose the `Checkout` model has an `is_paid` boolean attribute that should only be set by the payment gateway, or a `status` attribute that can be `pending`, `processing`, `shipped`, `delivered`, or `cancelled`. If these are not properly guarded, an attacker could try to bypass payment or prematurely mark an order as shipped.

Consider an attacker sending the following JSON payload to the `POST /checkouts` endpoint:

{
    "user_id": 1,
    "product_id": 101,
    "quantity": 1,
    "shipping_address": "123 Attacker St",
    "billing_address": "123 Attacker St",
    "payment_method": "credit_card",
    "is_paid": true, // Maliciously injected attribute
    "status": "shipped" // Maliciously injected attribute
}

If the `Checkout` model’s `$fillable` property doesn’t explicitly list `is_paid` and `status` (or if `$guarded = []` is used), and the controller uses `$request->all()`, these values would be directly assigned to the new `Checkout` record, potentially leading to unauthorized state changes.

Mitigation Strategies: The `fillable` and `guarded` Properties

Laravel’s Eloquent ORM provides two key properties on models to control mass assignment: `$fillable` and `$guarded`. The most secure approach is to use `$fillable` to explicitly define *only* the attributes that are intended to be mass-assigned.

Strategy 1: Using `$fillable` (Recommended)

Define an array of attributes that are safe to mass-assign. Any attribute not listed in `$fillable` will be ignored during mass assignment operations like `create()` and `fill()`. This is the most explicit and secure method.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Checkout extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'user_id',
        'product_id',
        'quantity',
        'shipping_address',
        'billing_address',
        'payment_method',
        'notes', // Example: user-provided notes are safe
    ];

    // Other model properties and methods...
}

With this configuration, the malicious `is_paid` and `status` attributes from the attacker’s request would be silently ignored by `Checkout::create($request->all())` because they are not present in the `$fillable` array. The `create` method would only use the attributes that are explicitly allowed.

Strategy 2: Using `$guarded`

The `$guarded` property defines attributes that should *not* be mass-assigned. If `$guarded` is set to `[‘*’]`, it means all attributes are guarded by default, and only those explicitly listed in `$fillable` (if `$fillable` is also defined) will be mass-assignable. If `$fillable` is *not* defined and `$guarded = [‘*’]` is used, then no attributes can be mass-assigned.

A common secure pattern is to use both `$fillable` and `$guarded`. If `$fillable` is defined, only those attributes are mass-assignable. If `$fillable` is *not* defined, then all attributes *except* those listed in `$guarded` are mass-assignable. The most restrictive and often preferred approach is to define `$fillable` and leave `$guarded` empty or set to `[‘*’]` to ensure only explicitly allowed fields are processed.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Checkout extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'user_id',
        'product_id',
        'quantity',
        'shipping_address',
        'billing_address',
        'payment_method',
        'notes',
    ];

    /**
     * The attributes that are not mass assignable.
     *
     * @var array
     */
    // If $fillable is defined, $guarded is effectively ignored for mass assignment.
    // However, it's good practice to define it for clarity or if $fillable is absent.
    // Setting it to ['*'] means "guard everything" if $fillable is not present.
    protected $guarded = ['id', 'created_at', 'updated_at']; // Guarding primary key and timestamps
}

In this setup, `id`, `created_at`, and `updated_at` are protected. If `$fillable` were absent, then any attribute *not* in `[‘id’, ‘created_at’, ‘updated_at’]` would be mass-assignable. However, because `$fillable` *is* present, only the attributes listed in `$fillable` are considered for mass assignment.

The Danger of `protected $guarded = [];`

Using `protected $guarded = [];` is a critical security anti-pattern. It explicitly states that *no* attributes are guarded, meaning *all* attributes submitted in a request are eligible for mass assignment. This is the most common way mass assignment vulnerabilities are introduced in Laravel applications.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Checkout extends Model
{
    // !!! DANGEROUS: Allows mass assignment of ALL attributes !!!
    protected $guarded = [];

    // ...
}

Refining Controller Logic for Enhanced Security

While securing the model is paramount, it’s also good practice to be judicious about what data is passed to the model. Instead of ` $request->all()`, consider explicitly selecting the validated and permitted data.

Strategy 3: Using `validate()` and `only()`/`except()`

Laravel’s `Request` object provides powerful validation and data filtering capabilities. Combining these with explicit attribute selection offers defense-in-depth.

<?php

namespace App\Http\Controllers;

use App\Models\Checkout;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class CheckoutController extends Controller
{
    public function store(Request $request)
    {
        // 1. Define validation rules
        $validator = Validator::make($request->all(), [
            'product_id'       => 'required|exists:products,id',
            'quantity'         => 'required|integer|min:1',
            'shipping_address' => 'required|string|max:255',
            'billing_address'  => 'required|string|max:255',
            'payment_method'   => 'required|in:credit_card,paypal,bank_transfer',
            'notes'            => 'nullable|string|max:1000',
            // Note: 'is_paid' and 'status' are intentionally omitted here
        ]);

        if ($validator->fails()) {
            return response()->json(['errors' => $validator->errors()], 422);
        }

        // 2. Get validated data
        $validatedData = $validator->validated();

        // 3. Add user_id (assuming authenticated user)
        $validatedData['user_id'] = auth()->id();

        // 4. Create the checkout record using only validated and authorized data
        // This is safe even if the Checkout model has $guarded = []
        // because we are only passing explicitly allowed fields.
        $checkout = Checkout::create($validatedData);

        // Alternative using $request->only() if validation is handled differently
        // $checkout = Checkout::create($request->only([
        //     'product_id', 'quantity', 'shipping_address', 'billing_address',
        //     'payment_method', 'notes'
        // ]) + ['user_id' => auth()->id()]);


        // ... process checkout ...

        return response()->json($checkout);
    }
}

In this refined approach:

  • We use Laravel’s Validator to ensure only expected fields are present and have correct formats. Crucially, sensitive fields like `is_paid` or `status` are not included in the validation rules, so they won’t be part of the validated data.
  • We explicitly add `user_id` from the authenticated user.
  • We pass the `$validatedData` array to `Checkout::create()`. This array contains only the data we’ve explicitly allowed and validated, making it safe for mass assignment regardless of the model’s `$fillable` or `$guarded` settings (though having proper model settings is still best practice).

Automated Security Auditing and Testing

To proactively identify and prevent mass assignment vulnerabilities:

Static Analysis

Tools like PHPStan or Psalm can be configured to detect potential mass assignment issues. For instance, you can create custom rules or use existing extensions that flag model definitions with `protected $guarded = [];` or controller methods that use `$request->all()` without apparent sanitization.

Automated Testing

Implement integration tests that specifically attempt to exploit mass assignment. These tests should:

  • Send requests with unexpected parameters (e.g., `is_admin`, `role`, `price`, `status`).
  • Assert that these parameters are ignored and do not affect the created/updated model.
  • Assert that the model’s state remains as expected (e.g., `is_admin` remains `false` for a regular user).

Example of a PHPUnit integration test:

<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class CheckoutMassAssignmentTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function it_prevents_mass_assignment_of_sensitive_fields()
    {
        $this->actingAs(User::factory()->create()); // Authenticate a user

        $payload = [
            'product_id'       => 1, // Assume product exists
            'quantity'         => 2,
            'shipping_address' => '123 Main St',
            'billing_address'  => '123 Main St',
            'payment_method'   => 'credit_card',
            'is_paid'          => true, // Sensitive field to test
            'status'           => 'shipped', // Sensitive field to test
            'user_id'          => 99, // Attempt to override user_id
        ];

        $response = $this->postJson('/checkouts', $payload);

        $response->assertStatus(201); // Or appropriate success status

        $checkout = json_decode($response->getContent(), true);

        // Assert that sensitive fields were NOT set by the payload
        $this->assertArrayNotHasKey('is_paid', $checkout);
        $this->assertArrayNotHasKey('status', $checkout);

        // Assert that user_id was set correctly from authenticated user, not overridden
        $this->assertEquals(auth()->id(), $checkout['user_id']);

        // Optionally, fetch from DB and assert
        $this->assertDatabaseHas('checkouts', [
            'user_id' => auth()->id(),
            'product_id' => 1,
            'quantity' => 2,
            // Check that is_paid and status are not present or have default values
            // This depends on your schema and default model behavior.
            // If they are nullable and not set, they might be null.
            // If they have defaults, check for those.
        ]);
    }
}

Conclusion

Mass assignment vulnerabilities are a critical security risk that can be easily overlooked in custom application logic, especially within complex models like those used for checkout processes. By diligently applying Laravel’s built-in security features, specifically by defining explicit `$fillable` attributes on your Eloquent models and by validating and sanitizing all incoming request data before it reaches your models, you can effectively mitigate this OWASP Top 10 threat. Continuous auditing and automated testing are essential to ensure these protections remain robust against evolving attack vectors.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Disaster Recovery 101: Architecting Auto-Failovers for Redis and PHP Deployments on OVH
  • How We Audited a High-Traffic WooCommerce Enterprise Stack on Google Cloud and Mitigated Race conditions during high-concurrency payment processing
  • Disaster Recovery 101: Architecting Auto-Failovers for Elasticsearch and Magento 2 Deployments on DigitalOcean
  • An Auditor’s Checklist for Securing WordPress Backends on OVH
  • Step-by-Step: Diagnosing Perl script high CPU throttling due to unoptimized regular expressions on AWS Servers

Copyright © 2026 · Vinay Vengala