mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge pull request #22363 from opf/merge-release/17.2-20260316105331
Merge release/17.2 into dev
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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's name. When that custom field was used in a Cost Report, the custom field'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.
|
||||
|
||||
|
||||
|
||||
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 -->
|
||||
@@ -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 |
|
||||
|
||||
@@ -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's name. When that custom field was used in a Cost Report, the custom field'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.
|
||||
|
||||
|
||||
|
||||
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 -->
|
||||
@@ -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's name. When that custom field was used in a Cost Report, the custom field'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.
|
||||
|
||||
|
||||
|
||||
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'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 -->
|
||||
@@ -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's name. When that custom field was used in a Cost Report, the custom field'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.
|
||||
|
||||
|
||||
|
||||
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.
|
||||
@@ -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 (0x00–0x1F, 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
|
||||
@@ -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)
|
||||
|
||||
+99
@@ -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("<img src=x onerror=alert(1)>")
|
||||
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("<img src=x onerror=alert(1)>")
|
||||
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
@@ -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
|
||||
|
||||
|
||||
@@ -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("<script>")
|
||||
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="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("<script>")
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:) }
|
||||
|
||||
Reference in New Issue
Block a user