Skip to content

Private work package subject/identity disclosure through the global Time Entries and Cost Entries APIs (linked work package rendered without visibility check)

Moderate
oliverguenther published GHSA-v3j7-vqwv-5w5q Jul 8, 2026

Package

bundler openproject (rubygems)

Affected versions

<= 17.5.1

Patched versions

17.6.0

Description

Summary

GET /api/v3/time_entries and GET /api/v3/cost_entries are instance/workspace-scope collection endpoints. Each entry links a Work Package through the API representer's associated_resource :work_package, which renders _links.workPackage.title (the Work Package subject) and _links.workPackage.href (the sequential Work Package id) without checking that the linked Work Package is visible to the requesting user.

The collection is authorized only by the time-entry / cost-entry project permission (view_time_entries / view_cost_entries), which is an independent project permission with no view_work_packages dependency. A user holding view_time_entries (or view_cost_entries) in a project, but not view_work_packages, therefore reads the subjects and ids of Work Packages they cannot access through the direct Work Package API (which returns 404 for those WPs).

This is the same disclosure class already fixed in GHSA-g387-6rm2-xw88 ("Private work package data disclosure through single meeting agenda item API", Medium, CWE-200/CWE-639), whose fix migrated the meeting-agenda-item representer's linked Work Package from the ungated associated_resource to the visibility-gated associated_visible_resource. The Time Entries and Cost Entries representers were not updated.

Details (all at tag v17.5.1)

The ungated leak

modules/costs/lib/api/v3/time_entries/time_entry_representer.rb:103:

associated_resource :work_package,
                    skip_render: ->(*) { represented.entity_type != "WorkPackage" },
                    link_property_name: :entity,
                    link_getter: :entity_id,
                    getter: ->(*) { represented.entity if represented.entity_type == "WorkPackage" }

modules/costs/lib/api/v3/cost_entries/cost_entry_representer.rb:48 is identical.

associated_resource (the ungated variant, lib/api/decorators/linked_resource.rb:169-197) renders the link with link_title_attribute: :name unconditionally. WorkPackage#name aliases subject (app/models/work_package.rb:358-360), so _links.workPackage.title is the Work Package subject. No visibility predicate is applied.

The fixed sibling that proves intent (GHSA-g387)

modules/meeting/lib/api/v3/meeting_agenda_items/meeting_agenda_item_representer.rb:80:

associated_visible_resource :work_package

associated_visible_resource (linked_resource.rb:199-217) adds skip_render on !visible?(current_user) and an undisclosed-link lambda. This is the canonical fix for this exact class.

The collection authorization omits the child visibility

The Index scope is TimeEntry.not_ongoing.visible(User.current) (modules/costs/app/models/queries/time_entries/time_entry_query.rb:40-46). TimeEntry.visible -> CostScopes#with_visible_entries_on (modules/costs/app/models/cost_scopes.rb:38-76) scopes only by the time-entry's own project_id via Project.allowed_to(user, :view_time_entries). It never references the linked Work Package's project or view_work_packages. view_time_entries/view_cost_entries are declared with no dependencies: :view_work_packages (modules/costs/lib/costs/engine.rb:39-41, :108-110), so a custom role can grant them while withholding view_work_packages.

The direct Work Package API enforces what the leak skips

lib/api/v3/work_packages/work_packages_api.rb:74-77:

@work_package = WorkPackage.visible.find(declared_params[:id])
authorize_in_work_package(:view_work_packages, work_package: @work_package) do
  raise API::Errors::NotFound.new model: :work_package

WorkPackage.visible = allowed_to(user, :view_work_packages) (app/models/work_package.rb:85). A user without view_work_packages on the WP gets 404 on the direct path.

Impact

A project member with view_time_entries (or view_cost_entries) but not view_work_packages reads the subject and id of every Work Package referenced by a time/cost entry in that project, despite being unable to open those Work Packages directly. Work Package subjects routinely encode sensitive information. Secondary cross-project reach exists when a Work Package is moved to an inaccessible project while time entries briefly retain their original project_id (app/services/work_packages/update_service.rb:136-145).

Affected versions

<= 17.5.1 (latest release). Community Edition (modules/costs is bundled in the AGPL CE, not EE-gated).

Remediation

Replace associated_resource :work_package with associated_visible_resource :work_package in time_entry_representer.rb:103 and cost_entry_representer.rb:48 (and audit lib/api/v3/custom_actions/custom_action_execute_representer.rb:36 for the same one-line change). This mirrors the GHSA-g387 fix.

Severity

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N (Medium), parity-anchored to GHSA-g387-6rm2-xw88 (same disclosure class, same vector).

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
Low
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:L/I:N/A:N

CVE ID

No known CVE

Weaknesses

Exposure of Sensitive Information to an Unauthorized Actor

The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. Learn more on MITRE.

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Credits