Files
timebank-cc-public/references/LIVEWIRE_METHOD_AUTHORIZATION_SECURITY.md
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

12 KiB

Livewire Method-Level Authorization Security

Date: 2026-01-03 Security Feature: Protection Against Livewire Direct Method Invocation Attacks Severity: Critical

Overview

This document details the comprehensive method-level authorization protection implemented across all admin management Livewire components to prevent unauthorized direct method invocation attacks.

Vulnerability Background

The Livewire Security Gap

Livewire components have a critical security consideration: the mount() method only runs once when a component is loaded. After that, any public method can be called directly via browser console using:

Livewire.find('component-id').call('methodName', parameters)

Problem: If authorization is only checked in mount(), an attacker can:

  1. Access a protected page (passing the mount() check)
  2. Call sensitive methods directly via browser console
  3. Bypass all authorization checks that only exist in mount()

Attack Scenario Example

// VULNERABLE CODE
class Manage extends Component
{
    public function mount()
    {
        // Only checks authorization on initial load
        if (!auth()->user()->isAdmin()) {
            abort(403);
        }
    }

    public function deleteProfile($id)
    {
        // NO authorization check here!
        Profile::find($id)->delete();
    }
}

Attack:

// Attacker loads the page (passes mount check)
// Then directly calls the method:
Livewire.find('profile-manage').call('deleteProfile', 123)
// Profile deleted without authorization!

Solution: RequiresAdminAuthorization Trait

Implementation

File: app/Http/Livewire/Traits/RequiresAdminAuthorization.php

<?php

namespace App\Http\Livewire\Traits;

use App\Helpers\ProfileAuthorizationHelper;

trait RequiresAdminAuthorization
{
    private $adminAuthorizationChecked = null;

    protected function authorizeAdminAccess(bool $forceRecheck = false): void
    {
        // Use cached result unless force recheck
        if (!$forceRecheck && $this->adminAuthorizationChecked === true) {
            return;
        }

        $activeProfileType = session('activeProfileType');
        $activeProfileId = session('activeProfileId');

        if (!$activeProfileType || !$activeProfileId) {
            abort(403, __('No active profile selected'));
        }

        $profile = $activeProfileType::find($activeProfileId);
        if (!$profile) {
            abort(403, __('Active profile not found'));
        }

        // Validate profile ownership using ProfileAuthorizationHelper
        ProfileAuthorizationHelper::authorize($profile);

        // Verify admin or central bank permissions
        if ($profile instanceof \App\Models\Admin) {
            $this->adminAuthorizationChecked = true;
            return;
        }

        if ($profile instanceof \App\Models\Bank) {
            if ($profile->level === 0) {
                $this->adminAuthorizationChecked = true;
                return;
            }
            abort(403, __('Central bank access required'));
        }

        abort(403, __('Admin or central bank access required'));
    }
}

Security Features

  1. ProfileAuthorizationHelper Integration: Uses centralized authorization helper
  2. Cross-Guard Attack Prevention: Validates that authenticated guard matches profile type
  3. Database-Level Validation: Verifies profile exists and user owns it
  4. Bank Level Validation: Only central bank (level=0) can access admin functions
  5. Performance Caching: Caches authorization result within same request
  6. IDOR Prevention: Prevents unauthorized access to other users' profiles

Protected Components

Complete Coverage (27 Methods Across 6 Components)

1. Posts/Manage.php (7 methods)

  • edit() - Edits post translation
  • create() - Creates new post
  • save() - Saves post data
  • deleteSelected() - Bulk deletes posts
  • undeleteSelected() - Bulk undeletes posts
  • stopPublication() - Stops post publication
  • startPublication() - Starts post publication

2. Categories/Manage.php (4 methods)

  • deleteSelected() - Bulk deletes categories
  • deleteCategory() - Deletes single category
  • updateCategory() - Updates category
  • storeCategory() - Creates new category

Additional Components:

  • Categories/Create.php - No methods to protect (view only)
  • Categories/ColorPicker.php - No methods to protect (UI only)

3. Tags/Manage.php (3 methods)

  • deleteTag() - Deletes single tag
  • deleteSelected() - Bulk deletes tags
  • updateTag() - Updates tag

4. Tags/Create.php (1 method)

  • create() - Creates new tag

5. Profiles/Manage.php (5 methods)

  • updateProfile() - Updates profile data
  • attachProfile() - Attaches profile to another profile
  • deleteProfile() - Deletes profile
  • restoreProfile() - Restores deleted profile
  • deleteSelected() - Bulk deletes profiles

6. Profiles/Create.php (1 method)

  • create() - Creates new profile (Critical fix - was unprotected)

Additional Components:

  • Profiles/ProfileTypesDropdown.php - No methods to protect (UI only)

7. Mailings/Manage.php (6 methods)

  • saveMailing() - Saves/creates mailing
  • deleteMailing() - Deletes single mailing
  • sendMailing() - Sends mailing to recipients
  • bulkDeleteMailings() - Bulk deletes mailings (Critical fix - was unprotected)
  • sendTestMail() - Sends test email
  • sendTestMailToSelected() - Sends test email to selected recipients

Additional Components:

  • Mailings/LocationFilter.php - No methods to protect (UI only)

Critical Vulnerabilities Fixed

1. Profiles/Create.php - Unauthorized Profile Creation

Severity: Critical Method: create() Risk: Allowed unauthorized users to create any profile type (User, Organization, Bank, Admin) Status: Fixed (line 391)

2. Mailings/Manage.php - Unauthorized Bulk Deletion

Severity: High Method: bulkDeleteMailings() Risk: Allowed unauthorized bulk deletion of mailings Status: Fixed (line 620)

Methods That Don't Need Protection

The following method types are intentionally left without authorization checks:

  1. Livewire Lifecycle Methods: mount(), render(), updated*(), updating*()
  2. Computed Properties: get*Property() methods (read-only)
  3. UI Helpers: Modal openers/closers, sorting, searching, filtering
  4. Event Listeners: Methods that only emit events or update UI state

Rationale: Even if users call these methods directly, they cannot execute dangerous operations without going through the protected methods.

Usage Pattern

Adding the Trait

<?php

namespace App\Http\Livewire\YourComponent;

use App\Http\Livewire\Traits\RequiresAdminAuthorization;
use Livewire\Component;

class YourComponent extends Component
{
    use RequiresAdminAuthorization;

    // ... component code
}

Protecting a Method

public function deleteItem($itemId)
{
    // CRITICAL: Authorize admin access at the start of the method
    $this->authorizeAdminAccess();

    // Now safe to perform the sensitive operation
    Item::find($itemId)->delete();

    $this->notification()->success(__('Item deleted'));
}

Force Recheck (Optional)

For methods that need fresh authorization (e.g., after significant time has passed):

public function criticalOperation()
{
    // Force fresh authorization check
    $this->authorizeAdminAccess(forceRecheck: true);

    // ... perform operation
}

Testing Requirements

All protected methods should have corresponding tests that verify:

  1. Unauthorized Access Blocked: Non-admin users cannot call the method
  2. Cross-Guard Attacks Blocked: Users authenticated to wrong guard cannot access
  3. IDOR Prevention: Users cannot access other users' data
  4. Authorization Success: Authorized admin users can successfully call the method

Example Test Structure

public function test_non_admin_cannot_delete_profile()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    $component = Livewire::test(Manage::class);

    $component->call('deleteProfile', 1)
        ->assertStatus(403);
}

public function test_admin_can_delete_profile()
{
    $admin = Admin::factory()->create();
    $this->actingAs($admin, 'admin');
    session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id]);

    $profile = User::factory()->create();

    $component = Livewire::test(Manage::class);

    $component->call('deleteProfile', $profile->id)
        ->assertHasNoErrors();

    $this->assertSoftDeleted('users', ['id' => $profile->id]);
}

Performance Considerations

Caching Mechanism

The trait implements request-scoped caching to avoid repeated authorization checks:

private $adminAuthorizationChecked = null;

protected function authorizeAdminAccess(bool $forceRecheck = false): void
{
    // Return cached result if already checked
    if (!$forceRecheck && $this->adminAuthorizationChecked === true) {
        return;
    }

    // ... perform authorization

    // Cache the result
    $this->adminAuthorizationChecked = true;
}

Benefits:

  • Multiple method calls in same request only check once
  • No database overhead for repeated checks
  • Optional force recheck for critical operations

Migration Guide

For Existing Components

  1. Add the trait: use RequiresAdminAuthorization;
  2. Identify sensitive methods: Any method that modifies data
  3. Add authorization call: $this->authorizeAdminAccess(); as first line
  4. Test thoroughly: Verify both authorized and unauthorized access

For New Components

  1. Include trait from start: Add RequiresAdminAuthorization trait
  2. Protect all data-modifying methods: Add authorization to create, update, delete operations
  3. Document method protection: Add comment indicating critical authorization
  4. Write tests: Include authorization tests from the beginning

Security Best Practices

1. Always Protect Data-Modifying Methods

// ✅ CORRECT
public function updateData($id, $value)
{
    $this->authorizeAdminAccess();
    Data::find($id)->update(['value' => $value]);
}

// ❌ WRONG
public function updateData($id, $value)
{
    // Missing authorization check!
    Data::find($id)->update(['value' => $value]);
}

2. UI Methods Don't Need Protection

// ✅ CORRECT - No protection needed for UI helper
public function openEditModal($id)
{
    $this->editId = $id;
    $this->showEditModal = true;
}

// ✅ CORRECT - Actual update is protected
public function saveEdit()
{
    $this->authorizeAdminAccess(); // Protection here!
    Data::find($this->editId)->update($this->editData);
}

3. Document Protected Methods

public function deleteItem($id)
{
    // CRITICAL: Authorize admin access for deletion
    $this->authorizeAdminAccess();

    Item::find($id)->delete();
}

Monitoring & Audit

Failed Authorization Logging

The ProfileAuthorizationHelper logs all failed authorization attempts with:

  • User ID attempting access
  • Profile they tried to access
  • Timestamp and IP address
  • Stack trace for debugging

Success Indicators

  • No authorization errors in logs
  • Users report proper access control
  • Security tests pass consistently
  • references/SECURITY_OVERVIEW.md - Overall security architecture
  • app/Helpers/ProfileAuthorizationHelper.php - Centralized authorization helper
  • references/ADMIN_MANAGEMENT_SECURITY_FIXES_2025-12-31.md - Previous security fixes

Summary

This comprehensive method-level authorization protection ensures that all sensitive admin operations require proper authorization on every method call, not just on component mount. This prevents Livewire direct method invocation attacks and provides defense-in-depth security for the admin management system.

Total Protected Methods: 27 Components Secured: 6 main management components Critical Vulnerabilities Fixed: 2 Security Level: Enterprise-grade protection against IDOR, cross-guard attacks, and direct method invocation