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
- ProjectBoard: Validate
$project_id against $this->projects->contains('id', $project_id) in mount() and selectProject().
- External access: Use
Hash::make() / Hash::check() for passwords. Remove credentials from logs.
- CreateTicket: Validate the
copy_from ticket belongs to a project the user has access to.
- EpicsOverview: Filter
loadEpics() to user's projects when no project is selected.
- canAccessPanel(): Require at least a basic role assignment.
- TicketCommentResource: Add
getEloquentQuery() with project membership scoping.
Disclosure
Found during a responsible security review. Happy to help with remediation.
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.phplines 76-78The
mount()method loads available projects filtered by membership (lines 63-73), but when a$project_idURL parameter is provided, it loads that project directly viaProject::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), andTicketTimeline::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.phplines 38-45External access passwords are stored without hashing.
ExternalLogin::login()at line 45 uses!==comparison instead ofHash::check().3. HIGH: Ticket Copy Bypasses Project Membership Scoping (CWE-639)
File:
app/Filament/Resources/Tickets/Pages/CreateTicket.phplines 18-37Ticket::find($copyFromId)at line 21 without authorization check. Any user withcreate_ticketpermission 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.phplines 87-101When
selectedProjectIdis 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.phplines 42-49,app/Models/User.phplines 102-105Google OAuth creates users with no role.
canAccessPanel()unconditionally returnstrue. 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.phplines 61-65Log::info()logs bothaccess_tokenand plaintextpasswordto application logs.7. MEDIUM: External Dashboard Sessions Have No Expiration (CWE-613)
File:
app/Livewire/ExternalLogin.phplines 53-57No session timeout for external access. Sessions persist indefinitely.
8. MEDIUM: TicketCommentResource Has No Query Scoping (CWE-862)
File:
app/Filament/Resources/TicketComments/TicketCommentResource.phpNo
getEloquentQuery()override — users withview_any_ticket::commentpermission see all comments across all projects.ProjectResourceandTicketResourceboth implement membership-based scoping, butTicketCommentResourcedoes not.Recommended Fixes
$project_idagainst$this->projects->contains('id', $project_id)inmount()andselectProject().Hash::make()/Hash::check()for passwords. Remove credentials from logs.copy_fromticket belongs to a project the user has access to.loadEpics()to user's projects when no project is selected.getEloquentQuery()with project membership scoping.Disclosure
Found during a responsible security review. Happy to help with remediation.