0

PHP open_basedir, disable_functions — hardening php.ini

Advanced5 min read·php-16-010
security

Concept

Mass assignment is a vulnerability where an attacker injects additional fields into a form submission to modify model attributes that shouldn't be user-settable (e.g., is_admin = 1, balance = 99999).

Laravel's protection mechanisms:

  • $fillable: Allowlist — only listed fields can be mass-assigned via create(), update(), fill().
  • $guarded: Blocklist — all fields EXCEPT listed ones can be mass-assigned. $guarded = [] disables protection entirely — dangerous.

Insecure direct object reference (IDOR): Accessing resources by ID without verifying ownership. User 1 accesses /orders/456 which belongs to User 2. Fix: always scope queries to the authenticated user: auth()->user()->orders()->findOrFail($id).

Directory traversal: Accessing files outside the intended directory via ../ sequences. Fix: use realpath() and verify the path starts with the allowed base directory.

Sensitive data exposure: Logging or returning sensitive data (passwords, API keys, PII). Fix: use $hidden on Eloquent models, use $casts to control serialization, implement toArray() to explicitly define what's exposed.

OWASP Top 10 (2021): Broken Access Control, Cryptographic Failures, Injection, Insecure Design, Security Misconfiguration, Vulnerable Components, Authentication Failures, Software Integrity Failures, Logging Failures, SSRF.

Code Example

php
<?php
// VULNERABLE — mass assignment
class User extends Model
{
    protected $fillable = []; // empty — no fillable defined
    protected $guarded = [];  // empty guarded = no protection!
}

$user = User::create($request->all()); // attacker adds is_admin=1 to form!

// SAFE — fillable allowlist
class User extends Model
{
    protected $fillable = ['name', 'email', 'password']; // ONLY these can be mass-assigned
    // is_admin, balance, role, etc. are NOT fillable
    protected $hidden = ['password', 'remember_token']; // hidden from ->toArray() / JSON
}

User::create($request->only(['name', 'email', 'password'])); // explicit too
User::create($request->validated()); // from validated Form Request

// IDOR fix — scope to authenticated user
// VULNERABLE
$order = Order::findOrFail($request->order_id); // any order, any user!

// SAFE — scoped to current user
$order = auth()->user()->orders()->findOrFail($request->order_id);
// or: Order::where('user_id', auth()->id())->findOrFail($request->order_id);

// Laravel gates and policies
class OrderPolicy
{
    public function view(User $user, Order $order): bool
    {
        return $user->id === $order->user_id;
    }
}
// In controller: $this->authorize('view', $order); // throws 403 if not authorized

// Sensitive data model — hide from serialization
class PaymentMethod extends Model
{
    protected $hidden = ['card_number', 'cvv'];

    // Only last 4 digits in JSON
    protected $appends = ['masked_number'];
    public function getMaskedNumberAttribute(): string
    {
        return '****' . substr($this->card_number, -4);
    }
}