Merge pull request #22363 from opf/merge-release/17.2-20260316105331

Merge release/17.2 into dev
This commit is contained in:
Klaus Zanders
2026-03-16 13:36:37 +01:00
committed by GitHub
27 changed files with 1053 additions and 58 deletions
-5
View File
@@ -137,30 +137,25 @@ jobs:
- platform: linux/amd64
digest: amd64-slim
bim_support: false
debian_base: trixie
target: slim
runner: runner=4cpu-linux-x64
- platform: linux/amd64
digest: amd64-bim
bim_support: true
debian_base: bookworm
target: slim-bim
runner: runner=4cpu-linux-x64
- platform: linux/arm64/v8
digest: arm64-slim
debian_base: trixie
bim_support: false
target: slim
runner: runner=4cpu-linux-arm64
- platform: linux/amd64
digest: amd64-aio
debian_base: bookworm
bim_support: true
target: all-in-one
runner: runner=4cpu-linux-x64
- platform: linux/arm64/v8
digest: arm64-aio
debian_base: trixie
bim_support: false
target: all-in-one
runner: runner=4cpu-linux-arm64
+5 -5
View File
@@ -446,11 +446,11 @@ class RepositoriesController < ApplicationController
end
def send_raw(content, path)
# Force the download
send_opt = { filename: filename_for_content_disposition(path.split("/").last) }
send_type = OpenProject::MimeType.of(path)
send_opt[:type] = send_type.to_s if send_type
send_data content, send_opt
# Force the download as binary to prevent CSP bypass
send_data content,
filename: filename_for_content_disposition(path.split("/").last),
type: "application/octet-stream",
disposition: :attachment
end
def render_text_entry
+30 -31
View File
@@ -114,13 +114,13 @@ module RepositoriesHelper
end
def render_changes_tree(tree)
return "" if tree.nil?
return "".html_safe if tree.nil?
output = +"<ul>"
tree.keys.sort.each do |file|
style = +"change"
items = tree.keys.sort.flat_map do |file|
style = "change"
text = File.basename(file)
if s = tree[file][:s]
if (s = tree[file][:s])
style += " folder"
path_param = without_leading_slash(to_path_param(@repository.relative_path(file)))
text = link_to(h(text),
@@ -129,38 +129,40 @@ module RepositoriesHelper
rev: @changeset.identifier),
title: I18n.t(:label_folder))
output += "<li class='#{style} icon icon-folder-#{calculate_folder_action(s)}'>#{text}</li>"
output += render_changes_tree(s)
elsif c = tree[file][:c]
folder_li = content_tag(:li, text,
class: "#{style} icon icon-folder-#{calculate_folder_action(s)}")
[folder_li, render_changes_tree(s)]
elsif (c = tree[file][:c])
style += " change-#{c.action}"
path_param = without_leading_slash(to_path_param(@repository.relative_path(c.path)))
unless c.action == "D"
title_text = changes_tree_change_title c.action
text_parts = []
unless c.action == "D"
text = link_to(h(text),
entry_revision_project_repository_path(project_id: @project,
repo_path: path_param,
rev: @changeset.identifier),
title: title_text)
title: changes_tree_change_title(c.action))
end
text << raw(" - #{h(c.revision)}") if c.revision.present?
text_parts << text
text_parts << " - " << h(c.revision) if c.revision.present?
if c.action == "M"
text << raw(" (" + link_to(I18n.t(:label_diff),
diff_revision_project_repository_path(project_id: @project,
repo_path: path_param,
rev: @changeset.identifier)) + ") ")
text_parts << " (" << link_to(I18n.t(:label_diff),
diff_revision_project_repository_path(project_id: @project,
repo_path: path_param,
rev: @changeset.identifier)) << ") "
end
text << raw(" " + content_tag("span", h(c.from_path), class: "copied-from")) if c.from_path.present?
text_parts << " " << content_tag(:span, c.from_path, class: "copied-from") if c.from_path.present?
output += changes_tree_li_element(c.action, text, style)
[changes_tree_li_element(c.action, safe_join(text_parts), style)]
end
end
output += "</ul>"
output.html_safe
end.compact
content_tag(:ul, safe_join(items))
end
def to_utf8_for_repositories(str)
@@ -296,19 +298,16 @@ module RepositoriesHelper
def changes_tree_li_element(action, text, style)
icon_name = case action
when "A"
"icon-add"
when "D"
"icon-delete"
when "C"
"icon-copy"
when "R"
"icon-rename"
when "A" then "icon-add"
when "D" then "icon-delete"
when "C" then "icon-copy"
when "R" then "icon-rename"
else
"icon-arrow-left-right"
end
"<li class='#{style} icon #{icon_name}'
title='#{changes_tree_change_title(action)}'>#{text}</li>"
content_tag(:li, text,
class: "#{style} icon #{icon_name}",
title: changes_tree_change_title(action))
end
end
+2
View File
@@ -32,6 +32,8 @@ class CustomField < ApplicationRecord
include CustomField::OrderStatements
include CustomField::CalculatedValue
normalizes :name, with: OpenProject::RemoveAsciiControlCharacters
has_many :custom_values, dependent: :delete_all
# WARNING: the inverse_of option is also required in order
# for the 'touch: true' option on the custom_field association in CustomOption
+10 -1
View File
@@ -31,7 +31,16 @@
# TODO: check if this can be postponed and if some plugins can make use of the ActiveSupport.on_load hooks
# Loads the core plugins located in lib_static/plugins
Dir.glob(Rails.root.join("lib_static/plugins/*")).each do |directory|
CORE_PLUGINS = %w[
acts_as_attachable
acts_as_customizable
acts_as_event
acts_as_journalized
acts_as_searchable
verification
].freeze
CORE_PLUGINS.map { |name| Rails.root.join("lib_static/plugins", name).to_s }.each do |directory|
if File.directory?(directory)
lib = File.join(directory, "lib")
@@ -0,0 +1,43 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class StripControlCharactersFromCustomFieldNames < ActiveRecord::Migration[7.1]
def up
execute <<~SQL.squish
UPDATE custom_fields
SET name = regexp_replace(name, E'[\\x01-\\x1F\\x7F]', '', 'g')
WHERE name ~ E'[\\x01-\\x1F\\x7F]'
SQL
end
def down
# Irreversible — stripped characters cannot be restored
end
end
+1 -1
View File
@@ -141,7 +141,7 @@ ENV PGDATA=/var/openproject/pgdata
COPY --from=openproject/gosu /go/bin/gosu /usr/local/bin/gosu
RUN chmod +x /usr/local/bin/gosu && gosu nobody true
COPY --from=openproject/hocuspocus:17.2.0 --chown=$APP_USER:$APP_USER /app /opt/hocuspocus
COPY --from=openproject/hocuspocus:17.2.1 --chown=$APP_USER:$APP_USER /app /opt/hocuspocus
# Keep node/npm in all-in-one for bundled hocuspocus even when BIM support is disabled.
COPY --from=build-base /usr/local/bin/node /usr/local/bin/node
COPY --from=build-base /usr/local/lib/node_modules /usr/local/lib/node_modules
+89
View File
@@ -0,0 +1,89 @@
---
title: OpenProject 16.6.9
sidebar_navigation:
title: 16.6.9
release_version: 16.6.9
release_date: 2026-03-16
---
# OpenProject 16.6.9
Release date: 2026-03-16
We released OpenProject [OpenProject 16.6.9](https://community.openproject.org/versions/2285).
The release contains several bug fixes and we recommend updating to the newest version.
Below you will find a complete list of all changes and bug fixes.
<!-- BEGIN CVE AUTOMATED SECTION -->
## Security fixes
### CVE-2026-32698 - SQL Injection via Custom Field Name can be chained to Remote Code Execution
OpenProject is vulnerable to an SQL injection attack via a custom field&#39;s name. When that custom field was used in a Cost Report, the custom field&#39;s name was injected into the SQL query without proper sanitation. This allowed an attacker to execute arbitrary SQL commands during the generation of a Cost Report.&nbsp;
As custom fields can only be generated by users with full administrator privileges, the attack surface is somewhat reduced.
Together with another bug in the _Repositories_ module, that used the project identifier without sanitation to generate the checkout path for a git repository in the filesystem, this allowed an attacker to checkout a git repository to an arbitrarily chosen path on the server. If the checkout is done within certain paths within the OpenProject application, upon the next restart of the application, this allows the attacker to inject ruby code into the application.
As the project identifier cannot be manually edited to any string containing special characters like dots or slashes, this needs to be changed via the SQL injection described above.
This vulnerability was reported by user [sam91281](https://yeswehack.com/hunters/sam91281) as part of the [YesWeHack.com OpenProject Bug Bounty program](https://yeswehack.com/programs/openproject), sponsored by the European Commission.
For more information, please see the [GitHub advisory #GHSA-jqhf-rf9x-9rhx](https://github.com/opf/openproject/security/advisories/GHSA-jqhf-rf9x-9rhx)
### CVE-2026-32703 - Repository files are served with the MIME type allowing them to be used to bypass Content Security Policy
When using the Repositories module in a project, it was possible to access the raw files via the browser with a URL like `/projects/{project}/repository/revisions/{commit_id}/raw/{file}.js.raw`. For those files, the MIME type was detected via the filename extension. For JavaScript and CSS files those files were then served from the same domain name as the application with the correct MIME type for active content and could be used to bypass the Content Security Policy. Together with other areas, where unsanitized HTML was served, this allowed persistent XSS attacks.
The MIME type detection for Repository files has been removed and files are served as `application/octet-stream` which will block their execution via the Content Security Policy.
Two places that could be used to abuse this vulnerability have been fixed:
The Repositories module did not properly escape filenames displayed from repositories. This allowed an attacker with push access into the repository to create commits with filenames that included HTML code that was injected in the page without proper sanitation. This allowed a persisted XSS attack against all members of this project that accessed the repositories page to display a changeset where the maliciously crafted file was deleted.
When a work package name contains HTML content and the work package is attached to a meeting, the work package name is rendered in the activities feed without proper sanitation.
All of those vulnerabilities were reported by user [sam91281](https://yeswehack.com/hunters/sam91281) as part of the [YesWeHack.com OpenProject Bug Bounty program](https://yeswehack.com/programs/openproject), sponsored by the European Commission.
For more information, please see the [GitHub advisory #GHSA-p423-72h4-fjvp](https://github.com/opf/openproject/security/advisories/GHSA-p423-72h4-fjvp)
<!-- END CVE AUTOMATED SECTION -->
<!--more-->
## Bug fixes and changes
<!-- Warning: Anything within the below lines will be automatically removed by the release script -->
<!-- BEGIN AUTOMATED SECTION -->
<!-- END AUTOMATED SECTION -->
<!-- Warning: Anything above this line will be automatically removed by the release script -->
+2 -1
View File
@@ -1,7 +1,7 @@
---
sidebar_navigation:
title: '16.x'
release_date: 2026-02-18
release_date: 2026-03-16
title: OpenProject Version 16 Release Notes
---
@@ -9,6 +9,7 @@ title: OpenProject Version 16 Release Notes
| Version | Release date |
|-------------------------------|--------------|
| [OpenProject 16.6.8](16-6-9/) | 2026-03-16 |
| [OpenProject 16.6.8](16-6-8/) | 2026-02-18 |
| [OpenProject 16.6.7](16-6-7/) | 2026-02-06 |
| [OpenProject 16.6.6](16-6-6/) | 2026-01-27 |
+89
View File
@@ -0,0 +1,89 @@
---
title: OpenProject 17.0.6
sidebar_navigation:
title: 17.0.6
release_version: 17.0.6
release_date: 2026-03-16
---
# OpenProject 17.0.6
Release date: 2026-03-16
We released OpenProject [OpenProject 17.0.6](https://community.openproject.org/versions/2286).
The release contains several bug fixes and we recommend updating to the newest version.
Below you will find a complete list of all changes and bug fixes.
<!-- BEGIN CVE AUTOMATED SECTION -->
## Security fixes
### CVE-2026-32698 - SQL Injection via Custom Field Name can be chained to Remote Code Execution
OpenProject is vulnerable to an SQL injection attack via a custom field&#39;s name. When that custom field was used in a Cost Report, the custom field&#39;s name was injected into the SQL query without proper sanitation. This allowed an attacker to execute arbitrary SQL commands during the generation of a Cost Report.&nbsp;
As custom fields can only be generated by users with full administrator privileges, the attack surface is somewhat reduced.
Together with another bug in the _Repositories_ module, that used the project identifier without sanitation to generate the checkout path for a git repository in the filesystem, this allowed an attacker to checkout a git repository to an arbitrarily chosen path on the server. If the checkout is done within certain paths within the OpenProject application, upon the next restart of the application, this allows the attacker to inject ruby code into the application.
As the project identifier cannot be manually edited to any string containing special characters like dots or slashes, this needs to be changed via the SQL injection described above.
This vulnerability was reported by user [sam91281](https://yeswehack.com/hunters/sam91281) as part of the [YesWeHack.com OpenProject Bug Bounty program](https://yeswehack.com/programs/openproject), sponsored by the European Commission.
For more information, please see the [GitHub advisory #GHSA-jqhf-rf9x-9rhx](https://github.com/opf/openproject/security/advisories/GHSA-jqhf-rf9x-9rhx)
### CVE-2026-32703 - Repository files are served with the MIME type allowing them to be used to bypass Content Security Policy
When using the Repositories module in a project, it was possible to access the raw files via the browser with a URL like `/projects/{project}/repository/revisions/{commit_id}/raw/{file}.js.raw`. For those files, the MIME type was detected via the filename extension. For JavaScript and CSS files those files were then served from the same domain name as the application with the correct MIME type for active content and could be used to bypass the Content Security Policy. Together with other areas, where unsanitized HTML was served, this allowed persistent XSS attacks.
The MIME type detection for Repository files has been removed and files are served as `application/octet-stream` which will block their execution via the Content Security Policy.
Two places that could be used to abuse this vulnerability have been fixed:
The Repositories module did not properly escape filenames displayed from repositories. This allowed an attacker with push access into the repository to create commits with filenames that included HTML code that was injected in the page without proper sanitation. This allowed a persisted XSS attack against all members of this project that accessed the repositories page to display a changeset where the maliciously crafted file was deleted.
When a work package name contains HTML content and the work package is attached to a meeting, the work package name is rendered in the activities feed without proper sanitation.
All of those vulnerabilities were reported by user [sam91281](https://yeswehack.com/hunters/sam91281) as part of the [YesWeHack.com OpenProject Bug Bounty program](https://yeswehack.com/programs/openproject), sponsored by the European Commission.
For more information, please see the [GitHub advisory #GHSA-p423-72h4-fjvp](https://github.com/opf/openproject/security/advisories/GHSA-p423-72h4-fjvp)
<!-- END CVE AUTOMATED SECTION -->
<!--more-->
## Bug fixes and changes
<!-- Warning: Anything within the below lines will be automatically removed by the release script -->
<!-- BEGIN AUTOMATED SECTION -->
<!-- END AUTOMATED SECTION -->
<!-- Warning: Anything above this line will be automatically removed by the release script -->
+91
View File
@@ -0,0 +1,91 @@
---
title: OpenProject 17.1.3
sidebar_navigation:
title: 17.1.3
release_version: 17.1.3
release_date: 2026-03-16
---
# OpenProject 17.1.3
Release date: 2026-03-16
We released OpenProject [OpenProject 17.1.3](https://community.openproject.org/versions/2282).
The release contains several bug fixes and we recommend updating to the newest version.
Below you will find a complete list of all changes and bug fixes.
<!-- BEGIN CVE AUTOMATED SECTION -->
## Security fixes
### CVE-2026-32698 - SQL Injection via Custom Field Name can be chained to Remote Code Execution
OpenProject is vulnerable to an SQL injection attack via a custom field&#39;s name. When that custom field was used in a Cost Report, the custom field&#39;s name was injected into the SQL query without proper sanitation. This allowed an attacker to execute arbitrary SQL commands during the generation of a Cost Report.&nbsp;
As custom fields can only be generated by users with full administrator privileges, the attack surface is somewhat reduced.
Together with another bug in the _Repositories_ module, that used the project identifier without sanitation to generate the checkout path for a git repository in the filesystem, this allowed an attacker to checkout a git repository to an arbitrarily chosen path on the server. If the checkout is done within certain paths within the OpenProject application, upon the next restart of the application, this allows the attacker to inject ruby code into the application.
As the project identifier cannot be manually edited to any string containing special characters like dots or slashes, this needs to be changed via the SQL injection described above.
This vulnerability was reported by user [sam91281](https://yeswehack.com/hunters/sam91281) as part of the [YesWeHack.com OpenProject Bug Bounty program](https://yeswehack.com/programs/openproject), sponsored by the European Commission.
For more information, please see the [GitHub advisory #GHSA-jqhf-rf9x-9rhx](https://github.com/opf/openproject/security/advisories/GHSA-jqhf-rf9x-9rhx)
### CVE-2026-32703 - Repository files are served with the MIME type allowing them to be used to bypass Content Security Policy
When using the Repositories module in a project, it was possible to access the raw files via the browser with a URL like `/projects/{project}/repository/revisions/{commit_id}/raw/{file}.js.raw`. For those files, the MIME type was detected via the filename extension. For JavaScript and CSS files those files were then served from the same domain name as the application with the correct MIME type for active content and could be used to bypass the Content Security Policy. Together with other areas, where unsanitized HTML was served, this allowed persistent XSS attacks.
The MIME type detection for Repository files has been removed and files are served as `application/octet-stream` which will block their execution via the Content Security Policy.
Two places that could be used to abuse this vulnerability have been fixed:
The Repositories module did not properly escape filenames displayed from repositories. This allowed an attacker with push access into the repository to create commits with filenames that included HTML code that was injected in the page without proper sanitation. This allowed a persisted XSS attack against all members of this project that accessed the repositories page to display a changeset where the maliciously crafted file was deleted.
When a work package name contains HTML content and the work package is attached to a meeting, the work package name is rendered in the activities feed without proper sanitation.
All of those vulnerabilities were reported by user [sam91281](https://yeswehack.com/hunters/sam91281) as part of the [YesWeHack.com OpenProject Bug Bounty program](https://yeswehack.com/programs/openproject), sponsored by the European Commission.
For more information, please see the [GitHub advisory #GHSA-p423-72h4-fjvp](https://github.com/opf/openproject/security/advisories/GHSA-p423-72h4-fjvp)
<!-- END CVE AUTOMATED SECTION -->
<!--more-->
## Bug fixes and changes
<!-- Warning: Anything within the below lines will be automatically removed by the release script -->
<!-- BEGIN AUTOMATED SECTION -->
- Bugfix: Internal error saving project list (when creating new one, or renaming an existing one) \[[#72362](https://community.openproject.org/wp/72362)\]
- Bugfix: Can&#39;t create automatically managed project folder when project name contains forbidden Nextcloud characters \[[#72525](https://community.openproject.org/wp/72525)\]
<!-- END AUTOMATED SECTION -->
<!-- Warning: Anything above this line will be automatically removed by the release script -->
+98
View File
@@ -0,0 +1,98 @@
---
title: OpenProject 17.2.1
sidebar_navigation:
title: 17.2.1
release_version: 17.2.1
release_date: 2026-03-16
---
# OpenProject 17.2.1
Release date: 2026-03-16
We released OpenProject [OpenProject 17.2.1](https://community.openproject.org/versions/2283).
The release contains several bug fixes and we recommend updating to the newest version.
Below you will find a complete list of all changes and bug fixes.
<!-- BEGIN CVE AUTOMATED SECTION -->
## Security fixes
### CVE-2026-32698 - SQL Injection via Custom Field Name can be chained to Remote Code Execution
OpenProject is vulnerable to an SQL injection attack via a custom field&#39;s name. When that custom field was used in a Cost Report, the custom field&#39;s name was injected into the SQL query without proper sanitation. This allowed an attacker to execute arbitrary SQL commands during the generation of a Cost Report.&nbsp;
As custom fields can only be generated by users with full administrator privileges, the attack surface is somewhat reduced.
Together with another bug in the _Repositories_ module, that used the project identifier without sanitation to generate the checkout path for a git repository in the filesystem, this allowed an attacker to checkout a git repository to an arbitrarily chosen path on the server. If the checkout is done within certain paths within the OpenProject application, upon the next restart of the application, this allows the attacker to inject ruby code into the application.
As the project identifier cannot be manually edited to any string containing special characters like dots or slashes, this needs to be changed via the SQL injection described above.
This vulnerability was reported by user [sam91281](https://yeswehack.com/hunters/sam91281) as part of the [YesWeHack.com OpenProject Bug Bounty program](https://yeswehack.com/programs/openproject), sponsored by the European Commission.
For more information, please see the [GitHub advisory #GHSA-jqhf-rf9x-9rhx](https://github.com/opf/openproject/security/advisories/GHSA-jqhf-rf9x-9rhx)
### CVE-2026-32703 - Repository files are served with the MIME type allowing them to be used to bypass Content Security Policy
When using the Repositories module in a project, it was possible to access the raw files via the browser with a URL like `/projects/{project}/repository/revisions/{commit_id}/raw/{file}.js.raw`. For those files, the MIME type was detected via the filename extension. For JavaScript and CSS files those files were then served from the same domain name as the application with the correct MIME type for active content and could be used to bypass the Content Security Policy. Together with other areas, where unsanitized HTML was served, this allowed persistent XSS attacks.
The MIME type detection for Repository files has been removed and files are served as `application/octet-stream` which will block their execution via the Content Security Policy.
Two places that could be used to abuse this vulnerability have been fixed:
The Repositories module did not properly escape filenames displayed from repositories. This allowed an attacker with push access into the repository to create commits with filenames that included HTML code that was injected in the page without proper sanitation. This allowed a persisted XSS attack against all members of this project that accessed the repositories page to display a changeset where the maliciously crafted file was deleted.
When a work package name contains HTML content and the work package is attached to a meeting, the work package name is rendered in the activities feed without proper sanitation.
All of those vulnerabilities were reported by user [sam91281](https://yeswehack.com/hunters/sam91281) as part of the [YesWeHack.com OpenProject Bug Bounty program](https://yeswehack.com/programs/openproject), sponsored by the European Commission.
For more information, please see the [GitHub advisory #GHSA-p423-72h4-fjvp](https://github.com/opf/openproject/security/advisories/GHSA-p423-72h4-fjvp)
<!-- END CVE AUTOMATED SECTION -->
<!--more-->
## Bug fixes and changes
<!-- Warning: Anything within the below lines will be automatically removed by the release script -->
<!-- BEGIN AUTOMATED SECTION -->
- Bugfix: Screenshot overlaps with CKeditor tool bar when scrolling \[[#72678](https://community.openproject.org/wp/72678)\]
- Bugfix: Jira import: error when imported type is the same as on OP (with mandatory custom fields) \[[#72854](https://community.openproject.org/wp/72854)\]
- Bugfix: Nextcloud: When creating an AMPF folder fails, other folders are also not created \[[#72940](https://community.openproject.org/wp/72940)\]
- Bugfix: Send test email fails when using SMTP and TLS \[[#73099](https://community.openproject.org/wp/73099)\]
- Bugfix: curl removed from openproject/openproject:17-slim in 17.2.0 \[[#73142](https://community.openproject.org/wp/73142)\]
<!-- END AUTOMATED SECTION -->
<!-- Warning: Anything above this line will be automatically removed by the release script -->
## Contributions
A big thanks to our Community members for reporting bugs and helping us identify and provide fixes.
This release, special thanks for reporting and finding bugs go to Martin Pfister.
+21
View File
@@ -13,6 +13,20 @@ Stay up to date and get an overview of the new features included in the releases
<!--- New release notes are generated below. Do not remove comment. -->
<!--- RELEASE MARKER -->
## 17.2.1
Release date: 2026-03-16
[Release Notes](17-2-1/)
## 17.1.3
Release date: 2026-03-16
[Release Notes](17-1-3/)
## 17.2.0
Release date: 2026-03-11
@@ -25,6 +39,13 @@ Release date: 2026-02-26
[Release Notes](17-1-2/)
## 17.0.6
Release date: 2026-03-16
[Release Notes](17-0-6/)
## 17.0.5
Release date: 2026-02-26
@@ -0,0 +1,10 @@
# frozen_string_literal: true
module OpenProject
# Strips ASCII control characters (0x000x1F, 0x7F) from a string.
# Designed for use with ActiveRecord's `normalizes` API:
#
# normalizes :name, with: OpenProject::RemoveAsciiControlCharacters
#
RemoveAsciiControlCharacters = ->(value) { value.is_a?(String) ? value.gsub(/[\x00-\x1F\x7F]/, "") : value }
end
+8 -3
View File
@@ -201,9 +201,14 @@ module OpenProject
end
def checkout_path
Pathname(OpenProject::Configuration.scm_local_checkout_path)
.join(@identifier)
.expand_path
root = Pathname(OpenProject::Configuration.scm_local_checkout_path).expand_path.to_s
path = Pathname(root).join(@identifier).expand_path.to_s
unless path.start_with?("#{root}/")
raise ArgumentError, "Checkout path escapes the configured root directory"
end
Pathname(path)
end
def checkout_uri
@@ -111,7 +111,14 @@ module OpenProject
# Used only in the creation of a repository, at a later point
# in time, it is referred to in the root_url
def managed_repository_path
File.join(self.class.managed_root, repository_identifier)
root = File.expand_path(self.class.managed_root)
path = File.expand_path(File.join(root, repository_identifier))
unless path.start_with?("#{root}/")
raise ArgumentError, "Repository path escapes the configured managed root directory"
end
path
end
##
@@ -1,4 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -44,8 +45,8 @@ class OpenProject::JournalFormatter::MeetingWorkPackageId < JournalFormatter::Ba
old = visible(values.first)
I18n.t(:"activity.item.meeting_agenda_item.work_package.updated#{html}",
value: new ? new.name : I18n.t(:label_agenda_item_undisclosed_wp, id: values.last),
old_value: old ? old.name : I18n.t(:label_agenda_item_undisclosed_wp, id: values.first))
value: new ? h(new.name) : I18n.t(:label_agenda_item_undisclosed_wp, id: values.last),
old_value: old ? h(old.name) : I18n.t(:label_agenda_item_undisclosed_wp, id: values.first))
end
def visible(work_package_id)
@@ -0,0 +1,99 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "spec_helper"
RSpec.describe OpenProject::JournalFormatter::MeetingWorkPackageId do
let(:instance) { described_class.new(build_stubbed(:journal)) }
let(:project) { create(:project) }
let(:old_work_package) { create(:work_package, project:, subject: "Old task") }
let(:new_work_package) { create(:work_package, project:, subject: "New task") }
describe "#render" do
context "when both work packages are visible" do
before do
allow(WorkPackage).to receive(:visible).and_return(WorkPackage.where(id: [old_work_package.id, new_work_package.id]))
end
it "renders work package names in HTML mode" do
result = instance.render("work_package_id", [old_work_package.id, new_work_package.id], html: true)
expect(result).to include("Old task")
expect(result).to include("New task")
end
it "renders work package names in plain text mode" do
result = instance.render("work_package_id", [old_work_package.id, new_work_package.id], html: false)
expect(result).to include("Old task")
expect(result).to include("New task")
end
end
context "when work packages are not visible" do
before do
allow(WorkPackage).to receive(:visible).and_return(WorkPackage.none)
end
it "renders undisclosed work package text with the ID" do
result = instance.render("work_package_id", [old_work_package.id, new_work_package.id], html: true)
expect(result).to include(old_work_package.id.to_s)
expect(result).to include(new_work_package.id.to_s)
expect(result).not_to include("Old task")
expect(result).not_to include("New task")
end
end
context "when work package names contain HTML" do
let(:old_work_package) { create(:work_package, project:, subject: "Safe task") }
let(:new_work_package) { create(:work_package, project:, subject: "<img src=x onerror=alert(1)>") }
before do
allow(WorkPackage).to receive(:visible).and_return(WorkPackage.where(id: [old_work_package.id, new_work_package.id]))
end
it "escapes HTML in work package names" do
result = instance.render("work_package_id", [old_work_package.id, new_work_package.id], html: true)
expect(result).not_to include("<img")
expect(result).to include("&lt;img src=x onerror=alert(1)&gt;")
end
it "also escapes in plain text mode since h() is applied unconditionally" do
result = instance.render("work_package_id", [old_work_package.id, new_work_package.id], html: false)
expect(result).not_to include("<img")
expect(result).to include("&lt;img src=x onerror=alert(1)&gt;")
end
end
end
end
@@ -113,7 +113,7 @@ module CostQuery::CustomFieldMixin
custom_options_table = CustomOption.table_name
<<-SQL
-- BEGIN Custom Field Join: #{db_field}
-- BEGIN Custom Field Join: cf_#{field.id}
LEFT OUTER JOIN (
SELECT
co.id AS #{db_field},
@@ -129,16 +129,16 @@ module CostQuery::CustomFieldMixin
AND #{db_field}.custom_field_id = #{field.id}
AND #{db_field}.customized_id = entries.entity_id
-- END Custom Field Join: #{db_field}
-- END Custom Field Join: cf_#{field.id}
SQL
end
def default_join_table(field)
<<-SQL % [CustomValue.table_name, table_name, field.id, field.name, SQL_TYPES[field.field_format]]
-- BEGIN Custom Field Join: "%4$s"
<<-SQL % [CustomValue.table_name, table_name, field.id, SQL_TYPES[field.field_format]]
-- BEGIN Custom Field Join: cf_%3$d
LEFT OUTER JOIN (
\tSELECT
\t\tCAST(value AS %5$s) AS %2$s,
\t\tCAST(value AS %4$s) AS %2$s,
\t\tcustomized_type,
\t\tcustom_field_id,
\t\tcustomized_id
@@ -148,7 +148,7 @@ module CostQuery::CustomFieldMixin
ON %2$s.customized_type = 'WorkPackage'
AND %2$s.custom_field_id = %3$d
AND %2$s.customized_id = entries.entity_id
-- END Custom Field Join: "%4$s"
-- END Custom Field Join: cf_%3$d
SQL
end
@@ -0,0 +1,36 @@
# frozen_string_literal: true
require_relative "../../spec_helper"
RSpec.describe CostQuery::CustomFieldMixin, :reporting_query_helper do
minimal_query
let!(:project) { create(:project_with_types) }
let!(:user) { create(:admin) }
describe "#default_join_table" do
let!(:custom_field) do
create(:wp_custom_field, :string, name: "Robert'); DROP TABLE Students;-- Roberts")
end
before do
CostQuery::Cache.reset!
CostQuery::Filter::CustomFieldEntries.all
end
after do
CostQuery::Cache.reset!
CostQuery::Filter::CustomFieldEntries.reset!
end
it "uses field.id in the SQL comment and does not include the field name" do
query.filter custom_field.attribute_name, operator: "=", value: "test"
sql = query.sql_statement.to_s
expect(sql).to include("-- BEGIN Custom Field Join: cf_#{custom_field.id}")
expect(sql).to include("-- END Custom Field Join: cf_#{custom_field.id}")
expect(sql).not_to include("DROP TABLE students")
expect(sql).to include("CAST(value AS varchar)")
end
end
end
+2 -2
View File
@@ -7,8 +7,8 @@ name: OpenProject
applicationSuite: openDesk
url: 'https://github.com/opf/openproject'
roadmap: 'https://www.openproject.org/roadmap'
releaseDate: '2026-03-11'
softwareVersion: '17.2.0'
releaseDate: '2026-03-16'
softwareVersion: '17.2.1'
developmentStatus: stable
softwareType: standalone/web
logo: 'publiccode_logo.svg'
@@ -287,6 +287,18 @@ RSpec.describe RepositoriesController do
end
end
describe "#entry" do
let(:permissions) { [:browse_repository] }
it "serves raw files as application/octet-stream attachment" do
get :entry, params: { project_id: project.identifier, repo_path: "subversion_test/textfile.txt", format: "raw" }
expect(response).to be_successful
expect(response.headers["Content-Type"]).to eq("application/octet-stream")
expect(response.headers["Content-Disposition"]).to match(/attachment/)
end
end
describe "checkout path" do
render_views
+278
View File
@@ -0,0 +1,278 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "spec_helper"
RSpec.describe RepositoriesHelper do
let(:project) { build_stubbed(:project) }
let(:repository) { build_stubbed(:repository_subversion, project:) }
let(:changeset) { build_stubbed(:changeset, repository:, revision: "42") }
before do
assign(:project, project)
assign(:repository, repository)
assign(:changeset, changeset)
allow(repository).to receive(:relative_path) { |path| path }
allow(helper).to receive_messages(
show_revisions_path_project_repository_path: "/revisions",
entry_revision_project_repository_path: "/entry",
diff_revision_project_repository_path: "/diff"
)
end
describe "#changes_tree_li_element" do
it "escapes plain text content" do
malicious_text = '<script>alert("xss")</script>'
result = helper.changes_tree_li_element("D", malicious_text, "change change-D")
expect(result).not_to include("<script>")
expect(result).to include("&lt;script&gt;")
end
it "preserves html_safe text content" do
safe_text = '<a href="/path">file.txt</a>'.html_safe
result = helper.changes_tree_li_element("A", safe_text, "change change-A")
expect(result).to include('<a href="/path">file.txt</a>')
end
it "escapes malicious content in the style parameter" do
result = helper.changes_tree_li_element("A", "file.txt", 'change" onclick="alert(1)')
expect(result).not_to include('onclick="alert(1)"')
expect(result).to include("onclick=&quot;alert(1)")
end
it "sets the correct icon class for each action" do
expect(helper.changes_tree_li_element("A", "f", "s")).to include("icon-add")
expect(helper.changes_tree_li_element("D", "f", "s")).to include("icon-delete")
expect(helper.changes_tree_li_element("C", "f", "s")).to include("icon-copy")
expect(helper.changes_tree_li_element("R", "f", "s")).to include("icon-rename")
expect(helper.changes_tree_li_element("M", "f", "s")).to include("icon-arrow-left-right")
end
it "sets the title attribute from the action" do
result = helper.changes_tree_li_element("D", "file.txt", "change")
expect(result).to include("title=\"#{I18n.t(:label_deleted)}\"")
end
end
describe "#render_changes_tree" do
def make_change(path:, action:, revision: nil, from_path: nil)
instance_double(Change, path:, action:, revision:, from_path:).tap do |c|
allow(c).to receive(:action=)
end
end
it "returns empty string for nil tree" do
expect(helper.render_changes_tree(nil)).to eq("")
end
context "with a deleted file containing a malicious name" do
let(:tree) do
{
'/"><img src=x onerror=alert(1)>' => {
c: make_change(path: '/"><img src=x onerror=alert(1)>', action: "D")
}
}
end
it "escapes the filename" do
result = helper.render_changes_tree(tree)
doc = Nokogiri::HTML.fragment(result)
expect(doc.at_css("img")).not_to be_present
li = doc.at_css("li")
expect(li.text).to include("<img src=x onerror=alert(1)>")
end
end
context "with an added file" do
let(:tree) do
{
"/added_file.txt" => {
c: make_change(path: "/added_file.txt", action: "A")
}
}
end
it "renders a link to the file" do
result = helper.render_changes_tree(tree)
expect(result).to include("<a")
expect(result).to include("added_file.txt")
expect(result).to include("icon-add")
end
end
context "with a modified file" do
let(:tree) do
{
"/modified.txt" => {
c: make_change(path: "/modified.txt", action: "M")
}
}
end
it "renders a diff link" do
result = helper.render_changes_tree(tree)
expect(result).to include(I18n.t(:label_diff))
expect(result).to include("icon-arrow-left-right")
end
end
context "with a file with revision" do
let(:tree) do
{
"/file.txt" => {
c: make_change(path: "/file.txt", action: "A", revision: "abc123")
}
}
end
it "includes the escaped revision" do
result = helper.render_changes_tree(tree)
expect(result).to include("abc123")
end
end
context "with a file with a malicious revision" do
let(:tree) do
{
"/file.txt" => {
c: make_change(path: "/file.txt", action: "A", revision: '<script>alert("xss")</script>')
}
}
end
it "escapes the revision" do
result = helper.render_changes_tree(tree)
expect(result).not_to include("<script>")
expect(result).to include("&lt;script&gt;")
end
end
context "with a copied file" do
let(:tree) do
{
"/copied.txt" => {
c: make_change(path: "/copied.txt", action: "C", from_path: "/original.txt")
}
}
end
it "renders the from_path in a span" do
result = helper.render_changes_tree(tree)
doc = Nokogiri::HTML.fragment(result)
span = doc.at_css("span.copied-from")
expect(span).to be_present
expect(span.text).to eq("/original.txt")
end
end
context "with a copied file with malicious from_path" do
let(:tree) do
{
"/copied.txt" => {
c: make_change(path: "/copied.txt", action: "C", from_path: "<img src=x onerror=alert(1)>")
}
}
end
it "escapes the from_path" do
result = helper.render_changes_tree(tree)
doc = Nokogiri::HTML.fragment(result)
expect(doc.at_css("img")).not_to be_present
span = doc.at_css("span.copied-from")
expect(span.text).to eq("<img src=x onerror=alert(1)>")
end
end
context "with a folder containing a malicious name" do
let(:tree) do
{
'/"><img src=x onerror=alert(1)>' => {
s: {
'/"><img src=x onerror=alert(1)>/file.txt' => {
c: make_change(path: '/"><img src=x onerror=alert(1)>/file.txt', action: "A")
}
}
}
}
end
it "escapes the folder name in the li element" do
result = helper.render_changes_tree(tree)
doc = Nokogiri::HTML.fragment(result)
expect(doc.at_css("img")).not_to be_present
folder_link = doc.at_css("li.folder a")
expect(folder_link.text).to include("<img src=x onerror=alert(1)>")
end
end
context "with a folder" do
let(:tree) do
{
"/src" => {
s: {
"/src/file.txt" => {
c: make_change(path: "/src/file.txt", action: "A")
}
}
}
}
end
it "renders nested ul structure" do
result = helper.render_changes_tree(tree)
doc = Nokogiri::HTML.fragment(result)
expect(doc.css("ul").count).to eq(2)
expect(doc.css("li").count).to eq(2)
end
it "renders a folder link with folder icon" do
result = helper.render_changes_tree(tree)
expect(result).to include("icon-folder-add")
expect(result).to include("folder")
end
end
end
end
@@ -0,0 +1,61 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe OpenProject::RemoveAsciiControlCharacters do
subject(:call) { described_class.call(value) }
context "with a clean string" do
let(:value) { "Hello World" }
it { is_expected.to eq("Hello World") }
end
context "with newline and tab characters" do
let(:value) { "Hello\n\tWorld\r\n" }
it { is_expected.to eq("HelloWorld") }
end
context "with null byte" do
let(:value) { "Hello\x00World" }
it { is_expected.to eq("HelloWorld") }
end
context "with escape and delete characters" do
let(:value) { "Hello\x1B\x7FWorld" }
it { is_expected.to eq("HelloWorld") }
end
context "with a mix of control characters" do
let(:value) { "\x01He\x02llo\x03 \x04Wo\x05rld\x06" }
it { is_expected.to eq("Hello World") }
end
context "with Unicode characters (preserved)" do
let(:value) { "Héllo Wörld 日本語" }
it { is_expected.to eq("Héllo Wörld 日本語") }
end
context "with spaces and quotes (preserved)" do
let(:value) { %{It's a "test" value} }
it { is_expected.to eq(%{It's a "test" value}) }
end
context "with a non-string value" do
let(:value) { 42 }
it { is_expected.to eq(42) }
end
context "with nil" do
let(:value) { nil }
it { is_expected.to be_nil }
end
end
@@ -550,6 +550,33 @@ RSpec.describe OpenProject::SCM::Adapters::Git do
end
end
describe "#checkout_path" do
let(:repos_dir) { Dir.mktmpdir }
before do
allow(OpenProject::Configuration)
.to receive(:scm_local_checkout_path)
.and_return(repos_dir)
end
after { FileUtils.remove_entry(repos_dir) }
it "returns a path within the configured root for a normal identifier" do
adapter = described_class.new("file:///some/repo.git", nil, nil, nil, nil, "my-project")
expect(adapter.checkout_path.to_s).to eq(File.join(repos_dir, "my-project"))
end
it "raises ArgumentError when the identifier contains path traversal" do
adapter = described_class.new("file:///some/repo.git", nil, nil, nil, nil, "../../etc")
expect { adapter.checkout_path }.to raise_error(ArgumentError, /escapes the configured root/)
end
it "raises ArgumentError for an identifier that resolves to the root itself" do
adapter = described_class.new("file:///some/repo.git", nil, nil, nil, nil, ".")
expect { adapter.checkout_path }.to raise_error(ArgumentError, /escapes the configured root/)
end
end
context "with a local repository" do
it_behaves_like "git adapter specs"
end
+10
View File
@@ -44,6 +44,16 @@ RSpec.describe CustomField do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(256) }
it "strips ASCII control characters on assignment" do
cf = build(:custom_field, name: "Test\nField\x00Name\t!")
expect(cf.name).to eq("TestFieldName!")
end
it "preserves Unicode and normal characters" do
cf = build(:custom_field, name: "Héllo Wörld 日本語")
expect(cf.name).to eq("Héllo Wörld 日本語")
end
describe "uniqueness" do
describe "WHEN value, locale and type are identical" do
before do
+12
View File
@@ -125,6 +125,18 @@ RSpec.describe Repository::Git do
end
end
context "and project with traversal identifier" do
before do
instance.project = project
allow(instance).to receive(:repository_identifier).and_return("../../etc/evil")
end
it "raises ArgumentError for path traversal" do
expect { instance.managed_repository_path }
.to raise_error(ArgumentError, /escapes the configured managed root/)
end
end
context "and associated project with parent" do
let(:parent) { build(:project) }
let(:project) { build(:project, parent:) }