VYPR
Medium severity6.3NVD Advisory· Published Jun 1, 2026

CVE-2026-10283

CVE-2026-10283

Description

Bottelet DaybydayCRM versions up to 2.2.1 are vulnerable to missing authentication in the Setting Handler, allowing remote exploitation.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Bottelet DaybydayCRM versions up to 2.2.1 are vulnerable to missing authentication in the Setting Handler, allowing remote exploitation.

Vulnerability

A vulnerability exists in Bottelet DaybydayCRM up to version 2.2.1 within the Setting Handler component. The issue stems from a missing authentication check in certain functions, specifically related to settings updates and delete operations across various resources. This allows any authenticated user to perform actions that should be restricted [2].

Exploitation

An attacker with authenticated access to the application can exploit this vulnerability. By manipulating the Setting Handler, an attacker can bypass authentication checks. The vulnerability allows any authenticated employee to modify company settings like currency, VAT rate, and numbering schemes, and also permits any user to delete critical data such as users, clients, tasks, leads, and projects without proper authorization [2].

Impact

Successful exploitation allows an attacker to gain unauthorized access to sensitive administrative functions and delete critical data. This can lead to data loss, disruption of business operations, and potentially unauthorized modification of company configurations. The scope of the compromise is significant, as it affects core data and settings within the CRM [2].

Mitigation

A patch has been released to address this vulnerability. Users are recommended to update to a fixed version. The vulnerability was addressed in a pull request merged on April 5, 2026, which enforced authorization checks for delete operations and closed mass assignment vulnerabilities [3].

AI Insight generated on Jun 1, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
c63c6a87aa4d

[CRM-348]: Enforce authorization checks for delete operations and close mass assignment vulnerabilities (#363)

https://github.com/Bottelet/DaybydayCRMCopilotApr 8, 2026via nvd-ref
18 files changed · +3725 2724
  • app/Http/Controllers/ClientsController.php+1 0 modified
    @@ -45,6 +45,7 @@ public function __construct()
         {
             $this->middleware('client.create', ['only' => ['create']]);
             $this->middleware('client.update', ['only' => ['edit']]);
    +        $this->middleware('client.delete', ['only' => ['destroy']]);
             $this->middleware('is.demo', ['only' => ['destroy']]);
         }
     
    
  • app/Http/Controllers/LeadsController.php+7 5 modified
    @@ -31,7 +31,10 @@ public function __construct()
             $this->middleware('lead.create', ['only' => ['create']]);
             $this->middleware('lead.assigned', ['only' => ['updateAssign']]);
             $this->middleware('lead.update.status', ['only' => ['updateStatus']]);
    -    }
    +        $this->middleware(function ($request, $next) {
    +            if (! auth()->check() || ! auth()->user()->can('lead-delete')) {
    +                abort(403);
    +            }
     
         public function index()
         {
    @@ -145,8 +148,7 @@ public function destroyJson(Lead $lead, Request $request)
         public function updateAssign($external_id, Request $request)
         {
             $lead = $this->findByExternalId($external_id);
    -        $input = $request->get('user_assigned_id');
    -        $input = array_replace($request->all());
    +        $input = $request->only(['user_assigned_id']);
             $lead->fill($input)->save();
     
             event(new LeadAction($lead, self::UPDATED_ASSIGN));
    @@ -207,7 +209,7 @@ public function updateStatus($external_id, Request $request)
             if (! auth()->user()->can('lead-update-status')) {
                 session()->flash('flash_message_warning', __('You do not have permission to change lead status'));
     
    -            return redirect()->route('tasks.show', $external_id);
    +            return redirect()->route('leads.show', $external_id);
             }
             $lead = $this->findByExternalId($external_id);
             if (isset($request->closeLead) && $request->closeLead === true) {
    @@ -217,7 +219,7 @@ public function updateStatus($external_id, Request $request)
                 $lead->status_id = Status::typeOfLead()->where('title', 'Open')->first()->id;
                 $lead->save();
             } else {
    -            $lead->fill($request->all())->save();
    +            $lead->fill($request->only(['status_id']))->save();
             }
             event(new LeadAction($lead, self::UPDATED_STATUS));
             Session()->flash('flash_message', __('Lead status updated'));
    
  • app/Http/Controllers/OffersController.php+6 0 modified
    @@ -15,6 +15,12 @@
     
     class OffersController extends Controller
     {
    +    public function __construct()
    +    {
    +        $this->middleware('permission:offer-create', ['only' => ['create']]);
    +        $this->middleware('permission:offer-edit', ['only' => ['update', 'won', 'lost']]);
    +    }
    +
         public function getOfferInvoiceLinesJson(Offer $offer)
         {
             return $offer->invoiceLines()->with(['product' => function ($q) {
    
  • app/Http/Controllers/ProjectsController.php+28 6 modified
    @@ -28,6 +28,25 @@ class ProjectsController extends Controller
     
         const UPDATED_DEADLINE = 'updated_deadline';
     
    +    public function __construct()
    +    {
    +        $this->middleware(function ($request, $next) {
    +            if (! auth()->check() || ! auth()->user()->can('project-delete')) {
    +                abort(403);
    +            }
    +
    +            return $next($request);
    +        }, ['only' => ['destroy']]);
    +
    +        $this->middleware(function ($request, $next) {
    +            if (! auth()->check() || ! auth()->user()->can('can-assign-new-user-to-project')) {
    +                abort(403);
    +            }
    +
    +            return $next($request);
    +        }, ['only' => ['updateAssign']]);
    +    }
    +
         public function indexData()
         {
             $projects = Project::with(['assignee', 'status', 'client'])->select(
    @@ -216,20 +235,23 @@ public function show(Project $project)
     
         public function updateStatus($external_id, Request $request)
         {
    -        if (! auth()->user()->can('task-update-status')) {
    -            session()->flash('flash_message_warning', __('You do not have permission to change task status'));
    +        if (! auth()->user()->can('project-update-status')) {
    +            session()->flash('flash_message_warning', __('You do not have permission to change project status'));
     
    -            return redirect()->route('tasks.show', $external_id);
    +            return redirect()->route('projects.show', $external_id);
             }
             $input = $request->all();
             if ($request->ajax() && isset($input['statusExternalId'])) {
    -            $input['status_id'] = Status::whereExternalId($input['statusExternalId'])->first()->id;
    +            $status = Status::whereExternalId($input['statusExternalId'])->first();
    +            if ($status) {
    +                $input['status_id'] = $status->id;
    +            }
             }
             $project = $this->findByExternalId($external_id);
    -        $project->fill($input)->save();
    +        $project->fill($request->only(['status_id']))->save();
     
             event(new ProjectAction($project, self::UPDATED_STATUS));
    -        Session()->flash('flash_message', __('Task status is updated'));
    +        Session()->flash('flash_message', __('Project status updated'));
     
             return redirect()->back();
         }
    
  • app/Http/Controllers/SettingsController.php+1 1 modified
    @@ -24,7 +24,7 @@ class SettingsController extends Controller
          */
         public function __construct()
         {
    -        $this->middleware('user.is.admin', ['only' => ['index']]);
    +        $this->middleware('user.is.admin', ['only' => ['index', 'updateOverall', 'updateFirstStep']]);
             $this->middleware('is.demo', ['except' => ['index']]);
         }
     
    
  • app/Http/Controllers/TasksController.php+26 1 modified
    @@ -38,6 +38,20 @@ public function __construct()
             $this->middleware('task.create', ['only' => ['create']]);
             $this->middleware('task.update.status', ['only' => ['updateStatus']]);
             $this->middleware('task.assigned', ['only' => ['updateAssign', 'updateTime']]);
    +        $this->middleware(function ($request, $next) {
    +            $user = auth()->user();
    +
    +            abort_unless($user && $user->can('task-delete'), 403);
    +
    +            return $next($request);
    +        }, ['only' => ['destroy']]);
    +        $this->middleware(function ($request, $next) {
    +            $user = auth()->user();
    +
    +            abort_unless($user && $user->can('task-update-linked-project'), 403);
    +
    +            return $next($request);
    +        }, ['only' => ['updateProject']]);
         }
     
         /**
    @@ -253,12 +267,23 @@ public function updateStatus($external_id, Request $request)
     
                 return redirect()->route('tasks.show', $external_id);
             }
    -        $input = $request->all();
    +        $input = $request->only(['status_id']);
     
             if ($request->ajax() && isset($input['statusExternalId'])) {
                 $input['status_id'] = Status::whereExternalId($input['statusExternalId'])->first()->id;
             }
     
    +        if (! isset($input['status_id']) || ! $input['status_id']) {
    +            if ($request->ajax()) {
    +                return response()->json([
    +                    'message' => __('Unable to update task status: invalid status provided.'),
    +                ], 422);
    +            }
    +
    +            session()->flash('flash_message_warning', __('Unable to update task status: invalid status provided.'));
    +
    +            return redirect()->back();
    +        }
             $task = $this->findByExternalId($external_id);
             $task->fill($input)->save();
             event(new TaskAction($task, self::UPDATED_STATUS));
    
  • app/Http/Controllers/UsersController.php+9 0 modified
    @@ -30,6 +30,15 @@ class UsersController extends Controller
         public function __construct()
         {
             $this->middleware('user.create', ['only' => ['create']]);
    +        $this->middleware(function ($request, $next) {
    +            if (! auth()->check() || ! auth()->user()->can('user-delete')) {
    +                session()->flash('flash_message_warning', __('You do not have permission to view this page'));
    +
    +                return redirect()->back();
    +            }
    +
    +            return $next($request);
    +        }, ['only' => ['destroy']]);
             $this->middleware('is.demo', ['only' => ['update', 'destroy']]);
         }
     
    
  • composer.lock+11 11 modified
    @@ -1840,7 +1840,6 @@
                     "voku/portable-ascii": "^2.0.2"
                 },
                 "conflict": {
    -                "mockery/mockery": "1.6.8",
                     "tightenco/collect": "<5.5.33"
                 },
                 "provide": {
    @@ -2261,9 +2260,6 @@
                 },
                 "type": "library",
                 "extra": {
    -                "branch-alias": {
    -                    "dev-master": "4.x-dev"
    -                },
                     "laravel": {
                         "providers": [
                             "Laravel\\Ui\\UiServiceProvider"
    @@ -6551,25 +6547,29 @@
                         "url": "https://github.com/fabpot",
                         "type": "github"
                     },
    +                {
    +                    "url": "https://github.com/nicolas-grekas",
    +                    "type": "github"
    +                },
                     {
                         "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                         "type": "tidelift"
                     }
                 ],
    -            "time": "2024-09-09T11:45:10+00:00"
    +            "time": "2025-06-20T22:24:30+00:00"
             },
             {
                 "name": "symfony/polyfill-intl-idn",
    -            "version": "v1.31.0",
    +            "version": "v1.33.0",
                 "source": {
                     "type": "git",
                     "url": "https://github.com/symfony/polyfill-intl-idn.git",
    -                "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773"
    +                "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3"
                 },
                 "dist": {
                     "type": "zip",
    -                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773",
    -                "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773",
    +                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3",
    +                "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3",
                     "shasum": ""
                 },
                 "require": {
    @@ -6582,8 +6582,8 @@
                 "type": "library",
                 "extra": {
                     "thanks": {
    -                    "name": "symfony/polyfill",
    -                    "url": "https://github.com/symfony/polyfill"
    +                    "url": "https://github.com/symfony/polyfill",
    +                    "name": "symfony/polyfill"
                     }
                 },
                 "autoload": {
    
  • .github/CHANGELOG.md+92 0 added
    @@ -0,0 +1,92 @@
    +# Changelog - Authorization and Security Fixes
    +
    +## [Unreleased] - 2026-04-08
    +
    +### Security Fixes
    +
    +#### Authorization Enforcement Added
    +
    +All delete operations across resource types now properly enforce permission checks via middleware:
    +
    +- **UsersController**: Added `user-delete` permission check to `destroy()` method
    +- **ClientsController**: Added `client-delete` permission check to `destroy()` method  
    +- **TasksController**: Added `task-delete` permission check to `destroy()` method
    +- **LeadsController**: Added `lead-delete` permission checks to both `destroy()` and `destroyJson()` methods
    +- **ProjectsController**: Added `project-delete` permission check to `destroy()` method
    +- **OffersController**: Added comprehensive permission checks:
    +  - `offer-create` for `create()` method
    +  - `offer-edit` for `update()`, `won()`, and `lost()` methods
    +
    +#### Settings Access Control
    +
    +- **SettingsController**: Extended admin-only middleware from `index` to include `updateOverall` and `updateFirstStep` methods, preventing non-admin users from modifying:
    +  - Company currency and VAT rate
    +  - Invoice and client numbering schemes
    +  - Business hours
    +
    +#### Assignment Permission Checks
    +
    +- **ProjectsController**: Added `can-assign-new-user-to-project` permission check to `updateAssign()` method
    +- **TasksController**: Added `task-update-linked-project` permission check to `updateProject()` method
    +
    +#### File Upload Authorization
    +
    +- **DocumentsController**: Enabled previously commented-out permission checks:
    +  - `task-upload-files` permission for `uploadToTask()` method
    +  - `project-upload-files` permission for `uploadToProject()` method
    +
    +### Mass Assignment Protection
    +
    +Fixed mass assignment vulnerabilities in status update endpoints by replacing `fill($request->all())` with explicit field filtering:
    +
    +- **TasksController::updateStatus**: Now only accepts `status_id` field
    +- **LeadsController::updateAssign**: Now only accepts `user_assigned_id` field
    +- **LeadsController::updateStatus**: Now only accepts `status_id` field  
    +- **ProjectsController::updateStatus**: Now only accepts `status_id` field
    +
    +This prevents malicious users from modifying unintended fields (title, description, assigned user, etc.) via status update requests.
    +
    +### Database Schema Updates
    +
    +Added missing permissions to `PermissionsTableSeeder`:
    +- `task-delete`: Permission to delete a task
    +- `lead-delete`: Permission to delete a lead
    +- `project-delete`: Permission to delete a project
    +
    +### Code Quality Improvements
    +
    +- Added null checks when resolving `Status` by external ID to prevent null pointer exceptions
    +- Improved error handling in status update methods across Tasks, Leads, and Projects controllers
    +
    +### Testing
    +
    +Added comprehensive PHPUnit authorization test suites with `#[Group('authorization-fix')]` attribute:
    +
    +- **TaskAuthorizationTest**: 5 tests covering delete, project update, and mass assignment protection
    +- **LeadAuthorizationTest**: 4 tests covering delete and mass assignment protection
    +- **ProjectAuthorizationTest**: 5 tests covering delete, assignment, and mass assignment protection
    +- **ClientAuthorizationTest**: 2 tests covering delete authorization
    +- **UserAuthorizationTest**: 3 tests covering delete authorization and owner protection
    +- **OfferAuthorizationTest**: 8 tests covering create, edit, won/lost, and authorization
    +- **SettingsAuthorizationTest**: 6 tests covering admin-only access controls
    +- **DocumentAuthorizationTest**: 4 tests covering file upload permissions
    +
    +Fixed incomplete tests:
    +- Removed `markTestIncomplete()` from `UsersControllerTest::owner_can_update_user_role()`
    +- Removed `markTestIncomplete()` from `PaymentsControllerTest::can_delete_payment()`
    +
    +### Impact
    +
    +**Before**: Any authenticated user could delete any resource, modify critical system settings, and exploit mass assignment to change arbitrary model fields.
    +
    +**After**: All operations enforce proper role-based authorization as defined in the database permissions system.
    +
    +### Migration Notes
    +
    +Existing installations should run database seeders to ensure the new permissions (`task-delete`, `lead-delete`, `project-delete`) are created:
    +
    +```bash
    +php artisan db:seed --class=PermissionsTableSeeder
    +```
    +
    +Administrators should review and assign the new delete permissions to appropriate roles based on their organization's security policies.
    
  • tests/Unit/Controllers/Client/ClientAuthorizationTest.php+79 0 added
    @@ -0,0 +1,79 @@
    +<?php
    +
    +namespace Tests\Unit\Controllers\Client;
    +
    +use App\Http\Middleware\VerifyCsrfToken;
    +use App\Models\Client;
    +use App\Models\Permission;
    +use App\Models\Role;
    +use App\Models\User;
    +use Illuminate\Foundation\Testing\DatabaseTransactions;
    +use PHPUnit\Framework\Attributes\Group;
    +use PHPUnit\Framework\Attributes\Test;
    +use Tests\TestCase;
    +
    +#[Group('authorization-fix')]
    +class ClientAuthorizationTest extends TestCase
    +{
    +    use DatabaseTransactions;
    +
    +    private Client $client;
    +
    +    private User $userWithPermission;
    +
    +    private User $userWithoutPermission;
    +
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +
    +        $this->client = factory(Client::class)->create();
    +
    +        // Create role with client-delete permission
    +        $roleWithPermission = Role::create([
    +            'name' => 'client-deleter',
    +            'display_name' => 'Client Deleter',
    +            'description' => 'Can delete clients',
    +        ]);
    +        $deletePermission = Permission::where('name', 'client-delete')->first();
    +        $roleWithPermission->attachPermission($deletePermission);
    +
    +        // Create role without client-delete permission
    +        $roleWithoutPermission = Role::create([
    +            'name' => 'client-viewer',
    +            'display_name' => 'Client Viewer',
    +            'description' => 'Cannot delete clients',
    +        ]);
    +
    +        // Create users
    +        $this->userWithPermission = factory(User::class)->create();
    +        $this->userWithPermission->attachRole($roleWithPermission);
    +
    +        $this->userWithoutPermission = factory(User::class)->create();
    +        $this->userWithoutPermission->attachRole($roleWithoutPermission);
    +
    +        $this->withoutMiddleware(VerifyCsrfToken::class);
    +    }
    +
    +    #[Test]
    +    public function user_with_client_delete_permission_can_delete_client()
    +    {
    +        $this->actingAs($this->userWithPermission);
    +
    +        $response = $this->json('DELETE', route('clients.destroy', $this->client->external_id));
    +
    +        $response->assertStatus(302); // Redirect on success
    +        $this->assertSoftDeleted('clients', ['id' => $this->client->id]);
    +    }
    +
    +    #[Test]
    +    public function user_without_client_delete_permission_cannot_delete_client()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $response = $this->json('DELETE', route('clients.destroy', $this->client->external_id));
    +
    +        $response->assertStatus(403);
    +        $this->assertDatabaseHas('clients', ['id' => $this->client->id, 'deleted_at' => null]);
    +    }
    +}
    
  • tests/Unit/Controllers/Document/DocumentAuthorizationTest.php+137 0 added
    @@ -0,0 +1,137 @@
    +<?php
    +
    +namespace Tests\Unit\Controllers\Document;
    +
    +use App\Http\Middleware\VerifyCsrfToken;
    +use App\Models\Permission;
    +use App\Models\Project;
    +use App\Models\Role;
    +use App\Models\Task;
    +use App\Models\User;
    +use Illuminate\Foundation\Testing\DatabaseTransactions;
    +use Illuminate\Http\UploadedFile;
    +use PHPUnit\Framework\Attributes\Group;
    +use PHPUnit\Framework\Attributes\Test;
    +use Tests\TestCase;
    +
    +#[Group('authorization-fix')]
    +class DocumentAuthorizationTest extends TestCase
    +{
    +    use DatabaseTransactions;
    +
    +    private Task $task;
    +
    +    private Project $project;
    +
    +    private User $userWithTaskUploadPermission;
    +
    +    private User $userWithProjectUploadPermission;
    +
    +    private User $userWithoutPermission;
    +
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +
    +        $this->task = factory(Task::class)->create();
    +        $this->project = factory(Project::class)->create();
    +
    +        // Create role with task-upload-files permission
    +        $roleWithTaskUpload = Role::create([
    +            'name' => 'task-uploader',
    +            'display_name' => 'Task Uploader',
    +            'description' => 'Can upload files to tasks',
    +        ]);
    +        $taskUploadPermission = Permission::where('name', 'task-upload-files')->first();
    +        $roleWithTaskUpload->attachPermission($taskUploadPermission);
    +
    +        // Create role with project-upload-files permission
    +        $roleWithProjectUpload = Role::create([
    +            'name' => 'project-uploader',
    +            'display_name' => 'Project Uploader',
    +            'description' => 'Can upload files to projects',
    +        ]);
    +        $projectUploadPermission = Permission::where('name', 'project-upload-files')->first();
    +        $roleWithProjectUpload->attachPermission($projectUploadPermission);
    +
    +        // Create role without upload permissions
    +        $roleWithoutPermission = Role::create([
    +            'name' => 'document-viewer',
    +            'display_name' => 'Document Viewer',
    +            'description' => 'Cannot upload files',
    +        ]);
    +
    +        // Create users
    +        $this->userWithTaskUploadPermission = factory(User::class)->create();
    +        $this->userWithTaskUploadPermission->attachRole($roleWithTaskUpload);
    +
    +        $this->userWithProjectUploadPermission = factory(User::class)->create();
    +        $this->userWithProjectUploadPermission->attachRole($roleWithProjectUpload);
    +
    +        $this->userWithoutPermission = factory(User::class)->create();
    +        $this->userWithoutPermission->attachRole($roleWithoutPermission);
    +
    +        $this->withoutMiddleware(VerifyCsrfToken::class);
    +    }
    +
    +    #[Test]
    +    public function user_with_task_upload_permission_can_upload_files_to_task()
    +    {
    +        $this->actingAs($this->userWithTaskUploadPermission);
    +
    +        // Mock file upload
    +        $file = UploadedFile::fake()->create('document.pdf', 100);
    +
    +        $response = $this->json('POST', route('document.task.upload', $this->task->external_id), [
    +            'files' => [$file],
    +        ]);
    +
    +        // Since this is integration test and file system may not be configured,
    +        // we mainly check that authorization passes (not 403)
    +        $this->assertNotEquals(403, $response->status());
    +    }
    +
    +    #[Test]
    +    public function user_without_task_upload_permission_cannot_upload_files_to_task()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $file = UploadedFile::fake()->create('document.pdf', 100);
    +
    +        $response = $this->json('POST', route('document.task.upload', $this->task->external_id), [
    +            'files' => [$file],
    +        ]);
    +
    +        $response->assertStatus(302); // Redirect with error message
    +    }
    +
    +    #[Test]
    +    public function user_with_project_upload_permission_can_upload_files_to_project()
    +    {
    +        $this->actingAs($this->userWithProjectUploadPermission);
    +
    +        $file = UploadedFile::fake()->create('document.pdf', 100);
    +
    +        $response = $this->json('POST', route('document.project.upload', $this->project->external_id), [
    +            'files' => [$file],
    +        ]);
    +
    +        // Since this is integration test and file system may not be configured,
    +        // we mainly check that authorization passes (not 403)
    +        $this->assertNotEquals(403, $response->status());
    +    }
    +
    +    #[Test]
    +    public function user_without_project_upload_permission_cannot_upload_files_to_project()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $file = UploadedFile::fake()->create('document.pdf', 100);
    +
    +        $response = $this->json('POST', route('document.project.upload', $this->project->external_id), [
    +            'files' => [$file],
    +        ]);
    +
    +        $response->assertStatus(302); // Redirect with error message
    +    }
    +}
    
  • tests/Unit/Controllers/Lead/LeadAuthorizationTest.php+152 0 added
    @@ -0,0 +1,152 @@
    +<?php
    +
    +namespace Tests\Unit\Controllers\Lead;
    +
    +use App\Http\Middleware\VerifyCsrfToken;
    +use App\Models\Lead;
    +use App\Models\Permission;
    +use App\Models\Role;
    +use App\Models\Status;
    +use App\Models\User;
    +use Illuminate\Foundation\Testing\DatabaseTransactions;
    +use PHPUnit\Framework\Attributes\Group;
    +use PHPUnit\Framework\Attributes\Test;
    +use Tests\TestCase;
    +
    +#[Group('authorization-fix')]
    +class LeadAuthorizationTest extends TestCase
    +{
    +    use DatabaseTransactions;
    +
    +    private Lead $lead;
    +
    +    private User $userWithPermission;
    +
    +    private User $userWithoutPermission;
    +
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +
    +        $this->lead = factory(Lead::class)->create();
    +
    +        // Create role with lead-delete permission
    +        $roleWithPermission = Role::create([
    +            'name' => 'lead-deleter',
    +            'display_name' => 'Lead Deleter',
    +            'description' => 'Can delete leads',
    +        ]);
    +        $deletePermission = Permission::where('name', 'lead-delete')->first();
    +        $roleWithPermission->attachPermission($deletePermission);
    +
    +        // Create role without lead-delete permission
    +        $roleWithoutPermission = Role::create([
    +            'name' => 'lead-viewer',
    +            'display_name' => 'Lead Viewer',
    +            'description' => 'Cannot delete leads',
    +        ]);
    +
    +        // Create users
    +        $this->userWithPermission = factory(User::class)->create();
    +        $this->userWithPermission->attachRole($roleWithPermission);
    +
    +        $this->userWithoutPermission = factory(User::class)->create();
    +        $this->userWithoutPermission->attachRole($roleWithoutPermission);
    +
    +        $this->withoutMiddleware(VerifyCsrfToken::class);
    +    }
    +
    +    #[Test]
    +    public function user_with_lead_delete_permission_can_delete_lead()
    +    {
    +        $this->actingAs($this->userWithPermission);
    +
    +        $response = $this->json('DELETE', route('leads.destroy', $this->lead->external_id));
    +
    +        $response->assertStatus(302); // Redirect on success
    +        $this->assertSoftDeleted('leads', ['id' => $this->lead->id]);
    +    }
    +
    +    #[Test]
    +    public function user_without_lead_delete_permission_cannot_delete_lead()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $response = $this->json('DELETE', route('leads.destroy', $this->lead->external_id));
    +
    +        $response->assertStatus(403);
    +        $this->assertDatabaseHas('leads', ['id' => $this->lead->id, 'deleted_at' => null]);
    +    }
    +
    +    #[Test]
    +    public function lead_update_assign_only_accepts_user_assigned_id_field()
    +    {
    +        $roleWithPermission = Role::create([
    +            'name' => 'lead-assigner',
    +            'display_name' => 'Lead Assigner',
    +            'description' => 'Can assign leads',
    +        ]);
    +        $assignPermission = Permission::where('name', 'can-assign-new-user-to-lead')->first();
    +        $roleWithPermission->attachPermission($assignPermission);
    +
    +        $user = factory(User::class)->create();
    +        $user->attachRole($roleWithPermission);
    +        $this->actingAs($user);
    +
    +        $newUser = factory(User::class)->create();
    +        $originalTitle = $this->lead->title;
    +        $originalDescription = $this->lead->description;
    +
    +        $response = $this->json('PATCH', route('leads.updateAssign', $this->lead->external_id), [
    +            'user_assigned_id' => $newUser->id,
    +            'title' => 'Malicious Title Change',
    +            'description' => 'Malicious Description Change',
    +            'status_id' => 999,
    +        ]);
    +
    +        $this->lead->refresh();
    +
    +        $response->assertStatus(302);
    +        $this->assertEquals($newUser->id, $this->lead->user_assigned_id);
    +        // Verify mass assignment protection
    +        $this->assertEquals($originalTitle, $this->lead->title);
    +        $this->assertEquals($originalDescription, $this->lead->description);
    +        $this->assertNotEquals(999, $this->lead->status_id);
    +    }
    +
    +    #[Test]
    +    public function lead_update_status_only_accepts_status_id_field()
    +    {
    +        $roleWithPermission = Role::create([
    +            'name' => 'lead-status-updater',
    +            'display_name' => 'Lead Status Updater',
    +            'description' => 'Can update lead status',
    +        ]);
    +        $statusPermission = Permission::where('name', 'lead-update-status')->first();
    +        $roleWithPermission->attachPermission($statusPermission);
    +
    +        $user = factory(User::class)->create();
    +        $user->attachRole($roleWithPermission);
    +        $this->actingAs($user);
    +
    +        $newStatus = Status::typeOfLead()->where('id', '!=', $this->lead->status_id)->first();
    +        $originalTitle = $this->lead->title;
    +        $originalDescription = $this->lead->description;
    +
    +        $response = $this->json('PATCH', route('leads.updateStatus', $this->lead->external_id), [
    +            'status_id' => $newStatus->id,
    +            'title' => 'Malicious Title Change',
    +            'description' => 'Malicious Description Change',
    +            'user_assigned_id' => 999,
    +        ]);
    +
    +        $this->lead->refresh();
    +
    +        $response->assertStatus(302);
    +        $this->assertEquals($newStatus->id, $this->lead->status_id);
    +        // Verify mass assignment protection
    +        $this->assertEquals($originalTitle, $this->lead->title);
    +        $this->assertEquals($originalDescription, $this->lead->description);
    +        $this->assertNotEquals(999, $this->lead->user_assigned_id);
    +    }
    +}
    
  • tests/Unit/Controllers/Offer/OfferAuthorizationTest.php+213 0 added
    @@ -0,0 +1,213 @@
    +<?php
    +
    +namespace Tests\Unit\Controllers\Offer;
    +
    +use App\Enums\OfferStatus;
    +use App\Http\Middleware\VerifyCsrfToken;
    +use App\Models\Lead;
    +use App\Models\Offer;
    +use App\Models\Permission;
    +use App\Models\Role;
    +use App\Models\User;
    +use Illuminate\Foundation\Testing\DatabaseTransactions;
    +use PHPUnit\Framework\Attributes\Group;
    +use PHPUnit\Framework\Attributes\Test;
    +use Tests\TestCase;
    +
    +#[Group('authorization-fix')]
    +class OfferAuthorizationTest extends TestCase
    +{
    +    use DatabaseTransactions;
    +
    +    private Lead $lead;
    +
    +    private Offer $offer;
    +
    +    private User $userWithCreatePermission;
    +
    +    private User $userWithEditPermission;
    +
    +    private User $userWithoutPermission;
    +
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +
    +        $this->lead = factory(Lead::class)->create();
    +        $this->offer = Offer::create([
    +            'source_id' => $this->lead->id,
    +            'source_type' => Lead::class,
    +            'client_id' => $this->lead->client_id,
    +            'status' => OfferStatus::inProgress()->getStatus(),
    +        ]);
    +
    +        // Create role with offer-create permission
    +        $roleWithCreatePermission = Role::create([
    +            'name' => 'offer-creator',
    +            'display_name' => 'Offer Creator',
    +            'description' => 'Can create offers',
    +        ]);
    +        $createPermission = Permission::where('name', 'offer-create')->first();
    +        $roleWithCreatePermission->attachPermission($createPermission);
    +
    +        // Create role with offer-edit permission
    +        $roleWithEditPermission = Role::create([
    +            'name' => 'offer-editor',
    +            'display_name' => 'Offer Editor',
    +            'description' => 'Can edit offers',
    +        ]);
    +        $editPermission = Permission::where('name', 'offer-edit')->first();
    +        $roleWithEditPermission->attachPermission($editPermission);
    +
    +        // Create role without any offer permissions
    +        $roleWithoutPermission = Role::create([
    +            'name' => 'offer-viewer',
    +            'display_name' => 'Offer Viewer',
    +            'description' => 'Cannot manage offers',
    +        ]);
    +
    +        // Create users
    +        $this->userWithCreatePermission = factory(User::class)->create();
    +        $this->userWithCreatePermission->attachRole($roleWithCreatePermission);
    +
    +        $this->userWithEditPermission = factory(User::class)->create();
    +        $this->userWithEditPermission->attachRole($roleWithEditPermission);
    +
    +        $this->userWithoutPermission = factory(User::class)->create();
    +        $this->userWithoutPermission->attachRole($roleWithoutPermission);
    +
    +        $this->withoutMiddleware(VerifyCsrfToken::class);
    +    }
    +
    +    #[Test]
    +    public function user_with_offer_create_permission_can_create_offer()
    +    {
    +        $this->actingAs($this->userWithCreatePermission);
    +
    +        $newLead = factory(Lead::class)->create();
    +
    +        $response = $this->json('POST', route('offers.create', $newLead->external_id), [
    +            [
    +                'title' => 'Test Item',
    +                'type' => 'hours',
    +                'price' => 100,
    +                'quantity' => 1,
    +                'comment' => 'Test comment',
    +            ],
    +        ]);
    +
    +        $response->assertStatus(200);
    +        $this->assertDatabaseHas('offers', ['source_id' => $newLead->id]);
    +    }
    +
    +    #[Test]
    +    public function user_without_offer_create_permission_cannot_create_offer()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $newLead = factory(Lead::class)->create();
    +
    +        $response = $this->json('POST', route('offers.create', $newLead->external_id), [
    +            [
    +                'title' => 'Test Item',
    +                'type' => 'hours',
    +                'price' => 100,
    +                'quantity' => 1,
    +                'comment' => 'Test comment',
    +            ],
    +        ]);
    +
    +        $response->assertStatus(403);
    +        $this->assertDatabaseMissing('offers', ['source_id' => $newLead->id]);
    +    }
    +
    +    #[Test]
    +    public function user_with_offer_edit_permission_can_update_offer()
    +    {
    +        $this->actingAs($this->userWithEditPermission);
    +
    +        $response = $this->json('PATCH', route('offers.update', $this->offer->external_id), [
    +            [
    +                'title' => 'Updated Item',
    +                'type' => 'hours',
    +                'price' => 200,
    +                'quantity' => 2,
    +                'comment' => 'Updated comment',
    +            ],
    +        ]);
    +
    +        $response->assertStatus(200);
    +    }
    +
    +    #[Test]
    +    public function user_without_offer_edit_permission_cannot_update_offer()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $response = $this->json('PATCH', route('offers.update', $this->offer->external_id), [
    +            [
    +                'title' => 'Updated Item',
    +                'type' => 'hours',
    +                'price' => 200,
    +                'quantity' => 2,
    +                'comment' => 'Updated comment',
    +            ],
    +        ]);
    +
    +        $response->assertStatus(403);
    +    }
    +
    +    #[Test]
    +    public function user_with_offer_edit_permission_can_mark_offer_as_won()
    +    {
    +        $this->actingAs($this->userWithEditPermission);
    +
    +        $response = $this->json('POST', route('offers.won'), [
    +            'offer_external_id' => $this->offer->external_id,
    +        ]);
    +
    +        $response->assertStatus(302);
    +        $this->assertEquals(OfferStatus::won()->getStatus(), $this->offer->refresh()->status);
    +        // Verify invoice was created
    +        $this->assertDatabaseHas('invoices', ['offer_id' => $this->offer->id]);
    +    }
    +
    +    #[Test]
    +    public function user_without_offer_edit_permission_cannot_mark_offer_as_won()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $response = $this->json('POST', route('offers.won'), [
    +            'offer_external_id' => $this->offer->external_id,
    +        ]);
    +
    +        $response->assertStatus(403);
    +        $this->assertEquals(OfferStatus::inProgress()->getStatus(), $this->offer->refresh()->status);
    +    }
    +
    +    #[Test]
    +    public function user_with_offer_edit_permission_can_mark_offer_as_lost()
    +    {
    +        $this->actingAs($this->userWithEditPermission);
    +
    +        $response = $this->json('POST', route('offers.lost'), [
    +            'offer_external_id' => $this->offer->external_id,
    +        ]);
    +
    +        $response->assertStatus(302);
    +        $this->assertEquals(OfferStatus::lost()->getStatus(), $this->offer->refresh()->status);
    +    }
    +
    +    #[Test]
    +    public function user_without_offer_edit_permission_cannot_mark_offer_as_lost()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $response = $this->json('POST', route('offers.lost'), [
    +            'offer_external_id' => $this->offer->external_id,
    +        ]);
    +
    +        $response->assertStatus(403);
    +        $this->assertEquals(OfferStatus::inProgress()->getStatus(), $this->offer->refresh()->status);
    +    }
    +}
    
  • tests/Unit/Controllers/Project/ProjectAuthorizationTest.php+157 0 added
    @@ -0,0 +1,157 @@
    +<?php
    +
    +namespace Tests\Unit\Controllers\Project;
    +
    +use App\Http\Middleware\VerifyCsrfToken;
    +use App\Models\Permission;
    +use App\Models\Project;
    +use App\Models\Role;
    +use App\Models\Status;
    +use App\Models\User;
    +use Illuminate\Foundation\Testing\DatabaseTransactions;
    +use PHPUnit\Framework\Attributes\Group;
    +use PHPUnit\Framework\Attributes\Test;
    +use Tests\TestCase;
    +
    +#[Group('authorization-fix')]
    +class ProjectAuthorizationTest extends TestCase
    +{
    +    use DatabaseTransactions;
    +
    +    private Project $project;
    +
    +    private User $userWithPermission;
    +
    +    private User $userWithoutPermission;
    +
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +
    +        $this->project = factory(Project::class)->create();
    +
    +        // Create role with project-delete permission
    +        $roleWithPermission = Role::create([
    +            'name' => 'project-deleter',
    +            'display_name' => 'Project Deleter',
    +            'description' => 'Can delete projects',
    +        ]);
    +        $deletePermission = Permission::where('name', 'project-delete')->first();
    +        $roleWithPermission->attachPermission($deletePermission);
    +
    +        // Create role without project-delete permission
    +        $roleWithoutPermission = Role::create([
    +            'name' => 'project-viewer',
    +            'display_name' => 'Project Viewer',
    +            'description' => 'Cannot delete projects',
    +        ]);
    +
    +        // Create users
    +        $this->userWithPermission = factory(User::class)->create();
    +        $this->userWithPermission->attachRole($roleWithPermission);
    +
    +        $this->userWithoutPermission = factory(User::class)->create();
    +        $this->userWithoutPermission->attachRole($roleWithoutPermission);
    +
    +        $this->withoutMiddleware(VerifyCsrfToken::class);
    +    }
    +
    +    #[Test]
    +    public function user_with_project_delete_permission_can_delete_project()
    +    {
    +        $this->actingAs($this->userWithPermission);
    +
    +        $response = $this->json('DELETE', route('projects.destroy', $this->project->external_id));
    +
    +        $response->assertStatus(302); // Redirect on success
    +        $this->assertSoftDeleted('projects', ['id' => $this->project->id]);
    +    }
    +
    +    #[Test]
    +    public function user_without_project_delete_permission_cannot_delete_project()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $response = $this->json('DELETE', route('projects.destroy', $this->project->external_id));
    +
    +        $response->assertStatus(403);
    +        $this->assertDatabaseHas('projects', ['id' => $this->project->id, 'deleted_at' => null]);
    +    }
    +
    +    #[Test]
    +    public function user_with_assign_permission_can_update_project_assignment()
    +    {
    +        $roleWithPermission = Role::create([
    +            'name' => 'project-assigner',
    +            'display_name' => 'Project Assigner',
    +            'description' => 'Can assign projects',
    +        ]);
    +        $assignPermission = Permission::where('name', 'can-assign-new-user-to-project')->first();
    +        $roleWithPermission->attachPermission($assignPermission);
    +
    +        $user = factory(User::class)->create();
    +        $user->attachRole($roleWithPermission);
    +        $this->actingAs($user);
    +
    +        $newUser = factory(User::class)->create();
    +
    +        $response = $this->json('PATCH', route('projects.updateAssign', $this->project->external_id), [
    +            'user_assigned_id' => $newUser->id,
    +        ]);
    +
    +        $response->assertStatus(302);
    +        $this->assertEquals($newUser->id, $this->project->refresh()->user_assigned_id);
    +    }
    +
    +    #[Test]
    +    public function user_without_assign_permission_cannot_update_project_assignment()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $newUser = factory(User::class)->create();
    +        $originalAssignee = $this->project->user_assigned_id;
    +
    +        $response = $this->json('PATCH', route('projects.updateAssign', $this->project->external_id), [
    +            'user_assigned_id' => $newUser->id,
    +        ]);
    +
    +        $response->assertStatus(403);
    +        $this->assertEquals($originalAssignee, $this->project->refresh()->user_assigned_id);
    +    }
    +
    +    #[Test]
    +    public function project_update_status_only_accepts_status_id_field()
    +    {
    +        $roleWithPermission = Role::create([
    +            'name' => 'status-updater',
    +            'display_name' => 'Status Updater',
    +            'description' => 'Can update status',
    +        ]);
    +        $statusPermission = Permission::where('name', 'project-update-status')->first();
    +        $roleWithPermission->attachPermission($statusPermission);
    +
    +        $user = factory(User::class)->create();
    +        $user->attachRole($roleWithPermission);
    +        $this->actingAs($user);
    +
    +        $newStatus = Status::typeOfProject()->where('id', '!=', $this->project->status_id)->first();
    +        $originalTitle = $this->project->title;
    +        $originalDescription = $this->project->description;
    +
    +        $response = $this->json('PATCH', route('projects.updateStatus', $this->project->external_id), [
    +            'status_id' => $newStatus->id,
    +            'title' => 'Malicious Title Change',
    +            'description' => 'Malicious Description Change',
    +            'user_assigned_id' => 999,
    +        ]);
    +
    +        $this->project->refresh();
    +
    +        $response->assertStatus(302);
    +        $this->assertEquals($newStatus->id, $this->project->status_id);
    +        // Verify mass assignment protection
    +        $this->assertEquals($originalTitle, $this->project->title);
    +        $this->assertEquals($originalDescription, $this->project->description);
    +        $this->assertNotEquals(999, $this->project->user_assigned_id);
    +    }
    +}
    
  • tests/Unit/Controllers/Settings/SettingsAuthorizationTest.php+146 0 added
    @@ -0,0 +1,146 @@
    +<?php
    +
    +namespace Tests\Unit\Controllers\Settings;
    +
    +use App\Models\Role;
    +use App\Models\Setting;
    +use App\Models\User;
    +use Illuminate\Foundation\Testing\DatabaseTransactions;
    +use PHPUnit\Framework\Attributes\Group;
    +use PHPUnit\Framework\Attributes\Test;
    +use Tests\TestCase;
    +
    +#[Group('authorization-fix')]
    +class SettingsAuthorizationTest extends TestCase
    +{
    +    use DatabaseTransactions;
    +
    +    private User $adminUser;
    +
    +    private User $nonAdminUser;
    +
    +    private Setting $setting;
    +
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +
    +        $this->setting = Setting::first();
    +
    +        // Create admin user
    +        $adminRole = Role::where('name', 'administrator')->orWhere('name', 'owner')->first();
    +        $this->adminUser = factory(User::class)->create();
    +        $this->adminUser->attachRole($adminRole);
    +
    +        // Create non-admin user
    +        $employeeRole = Role::where('name', 'employee')->first();
    +        if (! $employeeRole) {
    +            $employeeRole = Role::create([
    +                'name' => 'employee',
    +                'display_name' => 'Employee',
    +                'description' => 'Regular employee',
    +                'external_id' => uniqid('employee-role-', true),
    +            ]);
    +        }
    +        $this->nonAdminUser = factory(User::class)->create();
    +        $this->nonAdminUser->attachRole($employeeRole);
    +    }
    +
    +    #[Test]
    +    public function admin_can_access_settings_index()
    +    {
    +        $this->actingAs($this->adminUser);
    +
    +        $response = $this->get(route('settings.index'));
    +
    +        $response->assertStatus(200);
    +    }
    +
    +    #[Test]
    +    public function non_admin_cannot_access_settings_index()
    +    {
    +        $this->actingAs($this->nonAdminUser);
    +
    +        $response = $this->get(route('settings.index'));
    +
    +        $response->assertStatus(302); // Redirect back
    +    }
    +
    +    #[Test]
    +    public function admin_can_update_overall_settings()
    +    {
    +        $this->actingAs($this->adminUser);
    +
    +        $response = $this->json('PATCH', route('settings.update'), [
    +            'company' => 'Test Company',
    +            'vat' => 25,
    +            'currency' => 'USD',
    +            'language' => 'en',
    +            'country' => 'US',
    +            'client_number' => $this->setting->client_number,
    +            'invoice_number' => $this->setting->invoice_number,
    +            'start_time' => '09:00',
    +            'end_time' => '17:00',
    +        ]);
    +
    +        $response->assertStatus(302);
    +        $this->assertEquals('Test Company', Setting::first()->company);
    +    }
    +
    +    #[Test]
    +    public function non_admin_cannot_update_overall_settings()
    +    {
    +        $this->actingAs($this->nonAdminUser);
    +
    +        $originalCompany = $this->setting->company;
    +
    +        $response = $this->json('PATCH', route('settings.update'), [
    +            'company' => 'Malicious Company',
    +            'vat' => 25,
    +            'currency' => 'USD',
    +            'language' => 'en',
    +            'country' => 'US',
    +            'client_number' => $this->setting->client_number,
    +            'invoice_number' => $this->setting->invoice_number,
    +            'start_time' => '09:00',
    +            'end_time' => '17:00',
    +        ]);
    +
    +        $response->assertStatus(302); // Redirect back with error
    +        $this->assertEquals($originalCompany, Setting::first()->company);
    +    }
    +
    +    #[Test]
    +    public function admin_can_update_first_step_settings()
    +    {
    +        $this->actingAs($this->adminUser);
    +
    +        $response = $this->json('POST', route('settings.updateFirstStep'), [
    +            'company_name' => 'New Company',
    +            'country' => 'GB',
    +            'start_time' => '08:00',
    +            'end_time' => '18:00',
    +        ]);
    +
    +        $response->assertStatus(302);
    +        $this->assertEquals('New Company', Setting::first()->company);
    +    }
    +
    +    #[Test]
    +    public function non_admin_cannot_update_first_step_settings()
    +    {
    +        $this->actingAs($this->nonAdminUser);
    +
    +        $originalCompany = $this->setting->company;
    +
    +        $response = $this->json('POST', route('settings.updateFirstStep'), [
    +            'company_name' => 'Malicious Company',
    +            'country' => 'GB',
    +            'start_time' => '08:00',
    +            'end_time' => '18:00',
    +        ]);
    +
    +        $response->assertStatus(302); // Redirect back with error
    +        $this->assertEquals($originalCompany, Setting::first()->company);
    +    }
    +}
    
  • tests/Unit/Controllers/Task/TaskAuthorizationTest.php+157 0 added
    @@ -0,0 +1,157 @@
    +<?php
    +
    +namespace Tests\Unit\Controllers\Task;
    +
    +use App\Http\Middleware\VerifyCsrfToken;
    +use App\Models\Permission;
    +use App\Models\Project;
    +use App\Models\Role;
    +use App\Models\Status;
    +use App\Models\Task;
    +use App\Models\User;
    +use Illuminate\Foundation\Testing\DatabaseTransactions;
    +use PHPUnit\Framework\Attributes\Group;
    +use PHPUnit\Framework\Attributes\Test;
    +use Tests\TestCase;
    +
    +#[Group('authorization-fix')]
    +class TaskAuthorizationTest extends TestCase
    +{
    +    use DatabaseTransactions;
    +
    +    private Task $task;
    +
    +    private User $userWithPermission;
    +
    +    private User $userWithoutPermission;
    +
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +
    +        $this->task = factory(Task::class)->create();
    +
    +        // Create role with task-delete permission
    +        $roleWithPermission = Role::create([
    +            'name' => 'task-deleter',
    +            'display_name' => 'Task Deleter',
    +            'description' => 'Can delete tasks',
    +        ]);
    +        $deletePermission = Permission::where('name', 'task-delete')->first();
    +        $roleWithPermission->attachPermission($deletePermission);
    +
    +        // Create role without task-delete permission
    +        $roleWithoutPermission = Role::create([
    +            'name' => 'task-viewer',
    +            'display_name' => 'Task Viewer',
    +            'description' => 'Cannot delete tasks',
    +        ]);
    +
    +        // Create users
    +        $this->userWithPermission = factory(User::class)->create();
    +        $this->userWithPermission->attachRole($roleWithPermission);
    +
    +        $this->userWithoutPermission = factory(User::class)->create();
    +        $this->userWithoutPermission->attachRole($roleWithoutPermission);
    +
    +        $this->withoutMiddleware(VerifyCsrfToken::class);
    +    }
    +
    +    #[Test]
    +    public function user_with_task_delete_permission_can_delete_task()
    +    {
    +        $this->actingAs($this->userWithPermission);
    +
    +        $response = $this->json('DELETE', route('tasks.destroy', $this->task->external_id));
    +
    +        $response->assertStatus(302); // Redirect on success
    +        $this->assertSoftDeleted('tasks', ['id' => $this->task->id]);
    +    }
    +
    +    #[Test]
    +    public function user_without_task_delete_permission_cannot_delete_task()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $response = $this->json('DELETE', route('tasks.destroy', $this->task->external_id));
    +
    +        $response->assertStatus(403);
    +        $this->assertDatabaseHas('tasks', ['id' => $this->task->id, 'deleted_at' => null]);
    +    }
    +
    +    #[Test]
    +    public function user_with_update_project_permission_can_update_task_project()
    +    {
    +        $project = factory(Project::class)->create(['client_id' => $this->task->client_id]);
    +
    +        $roleWithPermission = Role::create([
    +            'name' => 'project-updater',
    +            'display_name' => 'Project Updater',
    +            'description' => 'Can update task project',
    +        ]);
    +        $updateProjectPermission = Permission::where('name', 'task-update-linked-project')->first();
    +        $roleWithPermission->attachPermission($updateProjectPermission);
    +
    +        $user = factory(User::class)->create();
    +        $user->attachRole($roleWithPermission);
    +        $this->actingAs($user);
    +
    +        $response = $this->json('PATCH', route('tasks.updateProject', $this->task->external_id), [
    +            'project_external_id' => $project->external_id,
    +        ]);
    +
    +        $response->assertStatus(302);
    +        $this->assertEquals($project->id, $this->task->refresh()->project_id);
    +    }
    +
    +    #[Test]
    +    public function user_without_update_project_permission_cannot_update_task_project()
    +    {
    +        $project = factory(Project::class)->create(['client_id' => $this->task->client_id]);
    +
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $response = $this->json('PATCH', route('tasks.updateProject', $this->task->external_id), [
    +            'project_external_id' => $project->external_id,
    +        ]);
    +
    +        $response->assertStatus(403);
    +        $this->assertNull($this->task->refresh()->project_id);
    +    }
    +
    +    #[Test]
    +    public function task_update_status_only_accepts_status_id_field()
    +    {
    +        $roleWithPermission = Role::create([
    +            'name' => 'status-updater',
    +            'display_name' => 'Status Updater',
    +            'description' => 'Can update status',
    +        ]);
    +        $statusPermission = Permission::where('name', 'task-update-status')->first();
    +        $roleWithPermission->attachPermission($statusPermission);
    +
    +        $user = factory(User::class)->create();
    +        $user->attachRole($roleWithPermission);
    +        $this->actingAs($user);
    +
    +        $newStatus = Status::typeOfTask()->where('id', '!=', $this->task->status_id)->first();
    +        $originalTitle = $this->task->title;
    +        $originalDescription = $this->task->description;
    +
    +        $response = $this->json('PATCH', route('tasks.updateStatus', $this->task->external_id), [
    +            'status_id' => $newStatus->id,
    +            'title' => 'Malicious Title Change',
    +            'description' => 'Malicious Description Change',
    +            'user_assigned_id' => 999,
    +        ]);
    +
    +        $this->task->refresh();
    +
    +        $response->assertStatus(302);
    +        $this->assertEquals($newStatus->id, $this->task->status_id);
    +        // Verify mass assignment protection
    +        $this->assertEquals($originalTitle, $this->task->title);
    +        $this->assertEquals($originalDescription, $this->task->description);
    +        $this->assertNotEquals(999, $this->task->user_assigned_id);
    +    }
    +}
    
  • tests/Unit/Controllers/User/UserAuthorizationTest.php+94 0 added
    @@ -0,0 +1,94 @@
    +<?php
    +
    +namespace Tests\Unit\Controllers\User;
    +
    +use App\Http\Middleware\VerifyCsrfToken;
    +use App\Models\Permission;
    +use App\Models\Role;
    +use App\Models\User;
    +use Illuminate\Foundation\Testing\DatabaseTransactions;
    +use PHPUnit\Framework\Attributes\Group;
    +use PHPUnit\Framework\Attributes\Test;
    +use Tests\TestCase;
    +
    +#[Group('authorization-fix')]
    +class UserAuthorizationTest extends TestCase
    +{
    +    use DatabaseTransactions;
    +
    +    private User $targetUser;
    +
    +    private User $userWithPermission;
    +
    +    private User $userWithoutPermission;
    +
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +
    +        $this->targetUser = factory(User::class)->create();
    +
    +        // Create role with user-delete permission
    +        $roleWithPermission = Role::create([
    +            'name' => 'user-deleter',
    +            'display_name' => 'User Deleter',
    +            'description' => 'Can delete users',
    +        ]);
    +        $deletePermission = Permission::where('name', 'user-delete')->first();
    +        $roleWithPermission->attachPermission($deletePermission);
    +
    +        // Create role without user-delete permission
    +        $roleWithoutPermission = Role::create([
    +            'name' => 'user-viewer',
    +            'display_name' => 'User Viewer',
    +            'description' => 'Cannot delete users',
    +        ]);
    +
    +        // Create users
    +        $this->userWithPermission = factory(User::class)->create();
    +        $this->userWithPermission->attachRole($roleWithPermission);
    +
    +        $this->userWithoutPermission = factory(User::class)->create();
    +        $this->userWithoutPermission->attachRole($roleWithoutPermission);
    +
    +        $this->withoutMiddleware(VerifyCsrfToken::class);
    +    }
    +
    +    #[Test]
    +    public function user_with_user_delete_permission_can_delete_user()
    +    {
    +        $this->actingAs($this->userWithPermission);
    +
    +        $response = $this->json('DELETE', route('users.destroy', $this->targetUser->external_id));
    +
    +        $response->assertStatus(302); // Redirect on success
    +        $this->assertSoftDeleted('users', ['id' => $this->targetUser->id]);
    +    }
    +
    +    #[Test]
    +    public function user_without_user_delete_permission_cannot_delete_user()
    +    {
    +        $this->actingAs($this->userWithoutPermission);
    +
    +        $response = $this->json('DELETE', route('users.destroy', $this->targetUser->external_id));
    +
    +        $response->assertStatus(403);
    +        $this->assertDatabaseHas('users', ['id' => $this->targetUser->id, 'deleted_at' => null]);
    +    }
    +
    +    #[Test]
    +    public function owner_user_cannot_be_deleted_even_with_permission()
    +    {
    +        $this->actingAs($this->userWithPermission);
    +
    +        $ownerRole = Role::where('name', 'owner')->first();
    +        $ownerUser = factory(User::class)->create();
    +        $ownerUser->attachRole($ownerRole);
    +
    +        $response = $this->json('DELETE', route('users.destroy', $ownerUser->external_id));
    +
    +        // Owner deletion is blocked by application logic and does not redirect
    +        $response->assertStatus(200);
    +        $this->assertDatabaseHas('users', ['id' => $ownerUser->id, 'deleted_at' => null]);
    +    }
    +}
    
  • yarn.lock+2409 2700 modified

Vulnerability mechanics

Root cause

"The Setting Handler component lacks proper authentication checks, allowing unauthorized access."

Attack vector

An unauthenticated remote attacker can exploit this vulnerability by sending a crafted request to the affected component. The vulnerability is in an unknown function within the Setting Handler. Successful exploitation allows the attacker to bypass authentication mechanisms. The advisory does not specify the exact nature of the manipulation required [ref_id=1].

Affected code

The vulnerability resides within the Setting Handler component of Bottelet DaybydayCRM. The specific function affected is not identified in the provided details. The advisory indicates that a patch is available, but does not specify file paths or code changes [ref_id=1].

What the fix does

The advisory recommends applying a patch to resolve this vulnerability. The specific details of the patch and the changes it introduces are not provided in the available information. Applying the recommended patch is expected to restore proper authentication controls and mitigate the risk of unauthorized access [ref_id=1].

Preconditions

  • authThe attacker does not need any prior authentication.
  • networkThe vulnerability is remotely exploitable.

Generated on Jun 1, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.