Skip to content

Security: ProjectBoard IDOR + 7 other authorization findings #58

Description

@lighthousekeeper1212

Summary

During a security review of dewakoding-project-management, I identified 8 authorization vulnerabilities (1 CRITICAL, 4 HIGH, 3 MEDIUM). The most significant is a 1-of-N inconsistency where some pages correctly validate project membership but others don't.

Findings

1. CRITICAL: ProjectBoard IDOR — Non-member Users Access Any Project's Board (CWE-639)

File: app/Filament/Pages/ProjectBoard.php lines 76-78

if ($project_id) {
    $this->selectedProjectId = (int) $project_id;
    $this->selectedProject = Project::find($project_id);  // No membership check!

The mount() method loads available projects filtered by membership (lines 63-73), but when a $project_id URL parameter is provided, it loads that project directly via Project::find() without checking membership. Similarly, selectProject() at line 117 has the same bypass.

1-of-N inconsistency: EpicsOverview::mount() at line 42 correctly validates $this->availableProjects->contains('id', $project_id), and TicketTimeline::selectProject() at line 108 correctly validates against $this->projects. ProjectBoard does not.

Impact: Any authenticated user can access any project's kanban board, view all tickets (names, descriptions, assignees, priorities, statuses), and view all member information.

2. HIGH: Plaintext Password Storage for External Dashboard (CWE-256)

File: app/Models/ExternalAccess.php lines 38-45

$access->password = Str::random(8);  // Stored as plaintext

External access passwords are stored without hashing. ExternalLogin::login() at line 45 uses !== comparison instead of Hash::check().

3. HIGH: Ticket Copy Bypasses Project Membership Scoping (CWE-639)

File: app/Filament/Resources/Tickets/Pages/CreateTicket.php lines 18-37

Ticket::find($copyFromId) at line 21 without authorization check. Any user with create_ticket permission can read any ticket's details via /admin/tickets/create?copy_from={ticket_id}.

1-of-N: TicketResource::getEloquentQuery() properly scopes ticket visibility by project membership. fillForm() bypasses this.

4. HIGH: EpicsOverview Leaks All Projects' Epics When No Project Selected (CWE-862)

File: app/Filament/Pages/EpicsOverview.php lines 87-101

public function loadEpics(): void
{
    // ...
    if ($this->selectedProjectId) {
        $query->where('project_id', $this->selectedProjectId);
    }
    $this->epics = $query->get();  // Returns ALL epics when no project selected
}

When selectedProjectId is null (initial load), the query returns all epics from all projects. mount() correctly validates the project_id parameter, but the "all epics" view has no membership scoping.

5. HIGH: Google OAuth + Registration Creates Users with Unrestricted Panel Access (CWE-269)

File: app/Http/Controllers/Auth/GoogleController.php lines 42-49, app/Models/User.php lines 102-105

Google OAuth creates users with no role. canAccessPanel() unconditionally returns true. Combined with ->registration() enabled, any user gains panel access.

6. MEDIUM: External Access Credentials Logged in Plaintext (CWE-532)

File: app/Filament/Resources/Projects/Pages/EditProject.php lines 61-65

Log::info() logs both access_token and plaintext password to application logs.

7. MEDIUM: External Dashboard Sessions Have No Expiration (CWE-613)

File: app/Livewire/ExternalLogin.php lines 53-57

No session timeout for external access. Sessions persist indefinitely.

8. MEDIUM: TicketCommentResource Has No Query Scoping (CWE-862)

File: app/Filament/Resources/TicketComments/TicketCommentResource.php

No getEloquentQuery() override — users with view_any_ticket::comment permission see all comments across all projects. ProjectResource and TicketResource both implement membership-based scoping, but TicketCommentResource does not.

Recommended Fixes

  1. ProjectBoard: Validate $project_id against $this->projects->contains('id', $project_id) in mount() and selectProject().
  2. External access: Use Hash::make() / Hash::check() for passwords. Remove credentials from logs.
  3. CreateTicket: Validate the copy_from ticket belongs to a project the user has access to.
  4. EpicsOverview: Filter loadEpics() to user's projects when no project is selected.
  5. canAccessPanel(): Require at least a basic role assignment.
  6. TicketCommentResource: Add getEloquentQuery() with project membership scoping.

Disclosure

Found during a responsible security review. Happy to help with remediation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions