Skip to content

Time entry update endpoint allows cross-organization modification of a known time-entry UUID

Moderate
Onatcer published GHSA-pmf9-pxq9-ccwr Apr 21, 2026

Package

No package listed

Affected versions

0.12.0

Patched versions

v0.12.1

Description

Summary

The PUT /api/v1/organizations/{organization}/time-entries/{timeEntry} API accepts a route-bound timeEntry from another organization when the caller has time-entries:update:all in the URL organization, allowing a known foreign time-entry UUID to be modified and rebound to objects in the caller's organization.

Details

TimeEntryController::checkPermission() only verifies time_entries.organization_id when the bound TimeEntry instance is passed into the helper. In the single-record update() path, the controller branches on time-entries:update:own vs time-entries:update:all but never passes the route-bound $timeEntry into that ownership check. By contrast, destroy() passes $timeEntry, and updateMultiple() first scopes the query to whereBelongsTo($organization, 'organization').

TimeEntryUpdateRequest validates the submitted member_id, project_id, task_id, and tags against the organization from the URL, but it does not validate that the route-bound timeEntry itself belongs to that organization. The update path then resolves project_id and task_id, calls fill($request->validated()), and saves the row. As a result, if the attacker knows a valid foreign time_entries.id, the following state change is accepted:

request URL organization: orgA
route-bound timeEntry row: orgB.time_entries[id = victim_uuid]
submitted project/task/tag/member: orgA-owned objects
result: orgB row is updated with orgA object references

This is a cross-tenant integrity issue rather than a purely local authorization smell. The application globally disables mass-assignment protection, the database only enforces single-column foreign keys, and the existing self-host consistency command does not check whether a time entry's organization_id matches its project, task, client, or member organization. After the save, the controller dispatches recalculation jobs for the old and new project/task, and those model relations aggregate time_entries by project_id or task_id without an additional organization filter. I also confirmed that report aggregation resolves project/task/client descriptors by ID without organization scoping, so polluted rows can affect project/task totals and grouped report labels. I did not identify a built-in low-privilege endpoint in this codebase that discloses foreign time_entries.id values, so the confirmed exploit boundary is limited to cases where that UUID is already known from some other source. Until a fix exists, the update path needs the same organization ownership check already present in destroy() or equivalent scoped binding enforcement.

PoC

  1. Create or use an authenticated account that has time-entries:update:all in organization orgA.
  2. Obtain any valid time_entries.id from another organization orgB. In this review I confirmed exploitation only for the known-ID case; I did not identify a built-in low-privilege disclosure path for foreign IDs.
  3. Pick any project_id and optional task_id that belong to orgA, then send:
PUT /api/v1/organizations/{orgA}/time-entries/{victim_uuid}
Content-Type: application/json

{
  "project_id": "{orgA_project_uuid}",
  "task_id": "{orgA_task_uuid}"
}
  1. Observe a successful update response instead of a forbidden response, and confirm the row state changes even though the row still belongs to orgB:
before: time_entries.id = victim_uuid, organization_id = orgB, project_id = old_orgB_project
after:  time_entries.id = victim_uuid, organization_id = orgB, project_id = orgA_project
  1. Wait for or run the recalculation jobs. The new orgA project/task totals now include the foreign row, and grouped report output can resolve descriptor names from the orgA objects attached to that orgB row.

Impact

A caller with elevated time-entry update rights in one organization can modify a known time-entry row from another organization and persist cross-tenant reference corruption. The confirmed impact is unauthorized integrity modification of another tenant's records plus downstream pollution of project/task totals and report group labels. I did not confirm a built-in path in this codebase to enumerate arbitrary foreign time-entry UUIDs, so broader exploitation depends on how such identifiers are exposed in the deployment.

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
High
Privileges required
High
User interaction
None
Scope
Changed
Confidentiality
None
Integrity
High
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:H/PR:H/UI:N/S:C/C:N/I:H/A:N

CVE ID

CVE-2026-42279

Weaknesses

Authorization Bypass Through User-Controlled Key

The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data. Learn more on MITRE.

Credits