Skip to content

Journal diff endpoint bypasses object, journal, and field visibility checks

High
oliverguenther published GHSA-f2rx-x2qj-2hgj Jun 8, 2026

Package

OpenProject

Affected versions

<= 17.3.2, <= 17.4.0

Patched versions

17.3.3, 17.4.1

Description

Title

Journal diff endpoint discloses hidden historical field values without enforcing object and field visibility

Description

Hi OpenProject security team,

I found an access-control issue in the Rails journal diff endpoint:

GET /journals/:journal_id/diff/:field

In my local OpenProject 17.4.0 and 17.3.1 test instances, this endpoint can disclose historical text values that the current user cannot normally access through the OpenProject API or UI.

The issue is that the diff endpoint authorizes access mainly through broad journable permissions, such as view_work_packages, view_project, or view_meetings. However, it does not consistently enforce the same object-level, journal-level, and field-level visibility checks used by the normal API and HTML routes.

This is not only a metadata leak. The response body contains the actual old and new values from journal details.

Affected versions tested

Runtime tested:

  • OpenProject 17.4.0
  • Docker image: openproject/openproject:17.4.0-slim

Also confirmed:

  • OpenProject 17.3.1

I did not determine the exact first affected version.

Affected endpoint

GET /journals/:journal_id/diff/:field

Confirmed affected diff fields include:

  • description
  • custom_fields_:custom_field_id
  • custom_comment_:custom_field_id
  • agenda_items_:agenda_item_id_notes

Main impact

A low-privileged user, and in supported public-project configurations an unauthenticated user, can retrieve hidden historical text values from journal diffs.

Confirmed disclosed data includes:

  • hidden work package description history
  • hidden work package text custom field history
  • restricted/internal work package journal diffs
  • admin-only work package custom field history
  • project custom field and custom comment history hidden from users without view_project_attributes
  • cancelled meeting agenda note history hidden from normal meeting routes

The historical aspect is important: even if a sensitive value was later removed from the current object, the journal diff can still expose the previous value.

Reproduction summary

I reproduced multiple variants locally.

1. Hidden work package journal disclosure

The actor can view only one individually shared work package in a private project. The actor cannot view another hidden work package in the same project.

Normal routes deny access to the hidden work package and its activity:

GET /api/v3/work_packages/:hidden_id -> 404
GET /work_packages/:hidden_id -> 404
GET /api/v3/work_packages/:hidden_id/activities -> 404
GET /api/v3/activities/:hidden_journal_id -> 404

But the journal diff route returns the hidden values:

GET /journals/:hidden_journal_id/diff/description -> 200
GET /journals/:hidden_journal_id/diff/custom_fields_:custom_field_id -> 200

The response contains the hidden old and new description/custom field values.

2. Restricted/internal journal disclosure

The actor can view the work package but does not have view_internal_comments.

Normal activity routes hide the internal journal:

GET /api/v3/work_packages/:id/activities -> 200, but the internal journal is omitted
GET /api/v3/activities/:internal_journal_id -> 404

But the diff route returns the internal diff:

GET /journals/:internal_journal_id/diff/description -> 200

The response contains the old and new internal text values.

3. Project custom field/custom comment disclosure

The actor has view_project but does not have view_project_attributes.

The normal project API hides the project custom field and custom comment values:

GET /api/v3/projects/:id -> 200, but hidden custom field values are absent

But the journal diff routes disclose them:

GET /journals/:project_journal_id/diff/custom_fields_:custom_field_id -> 200
GET /journals/:project_journal_id/diff/custom_comment_:custom_field_id -> 200

The response contains the hidden old and new custom field/custom comment values.

4. Unauthenticated public-project variants

I also reproduced supported public-project configurations where no account is required.

The modeled configuration was:

  • authentication is not required for public project access
  • the project is public
  • anonymous users can view the project, work package, or meeting
  • anonymous users do not have the more specific permission needed to see the hidden field or journal

Normal API/UI routes hide the protected values, but unauthenticated journal diff requests still return them.

Confirmed unauthenticated variants include:

GET /journals/:project_journal_id/diff/custom_fields_:custom_field_id -> 200
GET /journals/:project_journal_id/diff/custom_comment_:custom_field_id -> 200
GET /journals/:internal_journal_id/diff/description -> 200
GET /journals/:journal_id/diff/custom_fields_:custom_field_id -> 200
GET /journals/:meeting_journal_id/diff/agenda_items_:agenda_item_id_notes -> 200

These variants disclosed:

  • project custom field history hidden from anonymous users
  • project custom comment history hidden from anonymous users
  • restricted/internal work package journal diffs
  • admin-only work package custom field history
  • cancelled meeting agenda note history

Root cause notes

The vulnerable controller is:

app/controllers/journals_controller.rb

The diff action loads the journal directly:

Journal.find(params[:id])

The authorization logic maps only the journable type to a broad permission:

WorkPackage -> view_work_packages
Project     -> view_project
Meeting     -> view_meetings

This does not consistently enforce:

  • the journal's own visibility
  • the specific work package/object visibility
  • restricted/internal journal visibility
  • project custom field visibility through view_project_attributes
  • admin-only work package custom field hiding
  • meeting visibility scopes, including cancelled meeting exclusion

For project custom fields and custom comments, the current validation checks admin_only, but does not enforce the same field visibility used by the normal project API.

For work package custom fields, the admin_only check is also insufficient because WorkPackage does not declare admin_only_allowed, so an admin-only WorkPackageCustomField can be hidden from normal API rendering while still being rendered by the diff route.

Impact

This issue allows unauthorized disclosure of historical text values.

Confirmed disclosed data includes:

  • hidden work package descriptions
  • hidden text custom field values
  • restricted/internal journal contents
  • admin-only work package custom field values
  • project custom field and custom comment values
  • cancelled meeting agenda notes

Because journal diffs contain historical values, this can disclose information that is no longer present in the current object state.

Suggested severity

Suggested CWE:

  • CWE-862: Missing Authorization
  • CWE-200: Exposure of Sensitive Information to an Unauthorized Actor

Suggested CVSS v3.1:

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

I would rate this as High.

I am not claiming Critical because I did not reproduce integrity or availability impact. I also tested XSS-style payloads in the diff response, and they were escaped in my local test.

Suggested fix direction

The journal diff endpoint should enforce the same visibility rules as normal OpenProject read paths before rendering any diff.

A complete fix likely needs to:

  • require @journal.visible?(User.current) or equivalent before rendering any diff
  • ensure WorkPackage journals authorize against the actual work package entity, not only the project context
  • enforce restricted/internal journal visibility
  • enforce project custom field/custom comment visibility, including view_project_attributes
  • enforce admin-only WorkPackage custom field hiding for non-admin users
  • enforce normal Meeting visibility scopes, including cancelled meeting exclusion
  • avoid rendering a diff response body containing hidden marker strings when the normal API/UI route hides the same data

Scope

Testing was performed only on local OpenProject instances that I controlled. I did not test any third-party OpenProject instance, and I have not disclosed this publicly.

I attached a reproduction bundle with local scripts, clean/redacted outputs, source-level notes, and regression-test suggestions.

Attach Files

openproject_submission_bundle.zip

Severity

High

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
None
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:N/UI:N/S:U/C:H/I:N/A:N

CVE ID

CVE-2026-47193

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