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:
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
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:
In my local OpenProject
17.4.0and17.3.1test 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, orview_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:
17.4.0openproject/openproject:17.4.0-slimAlso confirmed:
17.3.1I did not determine the exact first affected version.
Affected endpoint
Confirmed affected diff fields include:
descriptioncustom_fields_:custom_field_idcustom_comment_:custom_field_idagenda_items_:agenda_item_id_notesMain 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:
view_project_attributesThe 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:
But the journal diff route returns the hidden values:
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:
But the diff route returns the internal diff:
The response contains the old and new internal text values.
3. Project custom field/custom comment disclosure
The actor has
view_projectbut does not haveview_project_attributes.The normal project API hides the project custom field and custom comment values:
But the journal diff routes disclose them:
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:
Normal API/UI routes hide the protected values, but unauthenticated journal diff requests still return them.
Confirmed unauthenticated variants include:
These variants disclosed:
Root cause notes
The vulnerable controller is:
The diff action loads the journal directly:
The authorization logic maps only the journable type to a broad permission:
This does not consistently enforce:
view_project_attributesFor 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_onlycheck is also insufficient becauseWorkPackagedoes not declareadmin_only_allowed, so an admin-onlyWorkPackageCustomFieldcan 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:
Because journal diffs contain historical values, this can disclose information that is no longer present in the current object state.
Suggested severity
Suggested CWE:
Suggested CVSS v3.1:
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:
@journal.visible?(User.current)or equivalent before rendering any diffview_project_attributesScope
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