416 lines
12 KiB
Markdown
416 lines
12 KiB
Markdown
# 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:
|
|
|
|
```javascript
|
|
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
|
|
|
|
```php
|
|
// 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**:
|
|
```javascript
|
|
// 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
|
|
<?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
|
|
<?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
|
|
|
|
```php
|
|
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):
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
// ✅ 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
|
|
|
|
```php
|
|
// ✅ 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
|
|
|
|
```php
|
|
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
|
|
|
|
## Related Documentation
|
|
|
|
- `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
|