Merge branch 'release/17.4' into dev

This commit is contained in:
OpenProject Actions CI
2026-05-12 14:21:47 +00:00
10 changed files with 191 additions and 12 deletions
+1
View File
@@ -27,6 +27,7 @@ targets:
- NODE_ENV=production
- NPM_CONFIG_PRODUCTION=false
- SECRET_KEY_BASE=1
- OPENPROJECT_DISABLE__SECRET_KEY_BASE__CHECK=true
dependencies:
- epel-release
- ImageMagick
@@ -0,0 +1,75 @@
# 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.
#++
# Refuse to boot in production if SECRET_KEY_BASE is a known-weak default that
# we have shipped in our Dockerfile or referenced in our documentation.
# An attacker who knows the secret can forge signed cookies and session data
# and decrypt anything derived from it.
insecure_secret_key_bases = [
"OVERWRITE_ME", # default value in Dockerfile
"secret", # old sample from documentation
"<your-secret-key-base>" # new sample from documentation
].freeze
no_rake_task = !(Rake.respond_to?(:application) && Rake.application.top_level_tasks.present?)
no_override = ENV["OPENPROJECT_DISABLE__SECRET_KEY_BASE__CHECK"] != "true"
if Rails.env.production? && no_rake_task && no_override
secret = ENV["SECRET_KEY_BASE"].to_s
fatal_reason =
if secret.empty?
"SECRET_KEY_BASE is not set."
elsif insecure_secret_key_bases.include?(secret)
"SECRET_KEY_BASE is set to a well-known default value (#{secret.inspect})."
end
if fatal_reason
abort <<~ERROR # rubocop:disable Rails/Exit
======= INSECURE SECRET_KEY_BASE DETECTED =======
#{fatal_reason}
OpenProject uses SECRET_KEY_BASE to sign cookies, sessions, and other
security-sensitive data. Running with a default or weak value allows
anyone to forge signed data and compromise the installation.
Generate a strong, random value (for example via `openssl rand -hex 64`)
and provide it via the SECRET_KEY_BASE environment variable. The same
value MUST be reused on every container/process start, otherwise
existing sessions and encrypted database content become unreadable.
If you know what you are doing and want to disable this check, set the environment
variable OPENPROJECT_DISABLE__SECRET_KEY_BASE__CHECK to "true" (not recommended).
Documentation:
- https://www.openproject.org/docs/installation-and-operations/installation/docker/
=================================================
ERROR
end
end
+1 -1
View File
@@ -32,4 +32,4 @@
# Specify a serializer for the signed and encrypted cookie jars.
# Valid options are :json, :marshal, and :hybrid.
Rails.application.config.action_dispatch.cookies_serializer = :marshal
Rails.application.config.action_dispatch.cookies_serializer = :json
+1 -1
View File
@@ -9,7 +9,7 @@ else
echo "Assets need to be compiled"
JOBS=8 npm install
SECRET_KEY_BASE=1 RAILS_ENV=production DATABASE_URL=nulldb://db \
SECRET_KEY_BASE="$(openssl rand -hex 64)" RAILS_ENV=production DATABASE_URL=nulldb://db \
bin/rails openproject:plugins:register_frontend assets:precompile
if [ "$DOCKER" = "1" ]; then
+1 -1
View File
@@ -49,7 +49,7 @@ gem 'stackprof'
Start thin via:
```shell
SECRET_KEY_BASE='abcd' RAILS_ENV=production CUSTOM_PLUGIN_GEMFILE=gemfile.profiling OPENPROJECT_RACK_PROFILER_ENABLED=true thin start
SECRET_KEY_BASE="$(openssl rand -hex 64)" RAILS_ENV=production CUSTOM_PLUGIN_GEMFILE=gemfile.profiling OPENPROJECT_RACK_PROFILER_ENABLED=true thin start
```
## Using the profiling tools
@@ -71,7 +71,7 @@ following command:
```shell
docker run -it -p 8080:80 \
-e SECRET_KEY_BASE=secret \
-e SECRET_KEY_BASE=<your-secret-key-base> \
-e OPENPROJECT_HOST__NAME=localhost:8080 \
-e OPENPROJECT_HTTPS=false \
-e OPENPROJECT_DEFAULT__LANGUAGE=en \
@@ -81,7 +81,7 @@ docker run -it -p 8080:80 \
Explanation of the used configuration values:
- `-p 8080:80` binds the port 80 of the container to 8080 on the machine running docker.
- `SECRET_KEY_BASE` sets the secret key base for Rails. Please use a pseudo-random value for this and treat it like a password.
- `SECRET_KEY_BASE` sets the secret key base for Rails. Replace `<your-secret-key-base>` with a strong, random value (for example generated with `openssl rand -hex 64`). Treat it like a password and **store it securely** — the same value must be reused on every container start, otherwise existing sessions and encrypted database content become unreadable. OpenProject will refuse to start with a default or weak value.
- `OPENPROJECT_HOST__NAME` sets the host name of the application. This value is used for generating forms and links in emails, and needs to match the external request host name (The value users are seeing in their browsers).
- `OPENPROJECT_HTTPS=false` disables the on-by-default HTTPS mode of OpenProject so you can access the instance over HTTP-only. For all production systems we strongly advise not to set this to false, and instead set up a proper TLS/SSL termination on your outer web server.
- `OPENPROJECT_DEFAULT__LANGUAGE` does two things. It controls for the very first installation, in which language basic data (such as types, status names, etc.) and demo data is being created in. It also sets the default fallback language for new users.
@@ -102,7 +102,7 @@ achieved with the `-d` flag:
```shell
docker run -d -p 8080:80 \
-e SECRET_KEY_BASE=secret \
-e SECRET_KEY_BASE=<your-secret-key-base> \
-e OPENPROJECT_HOST__NAME=localhost:8080 \
-e OPENPROJECT_HTTPS=false \
openproject/openproject:17
@@ -134,7 +134,7 @@ sudo mkdir -p /var/lib/openproject/{pgdata,assets}
docker run -d -p 8080:80 --name openproject \
-e OPENPROJECT_HOST__NAME=openproject.example.com \
-e SECRET_KEY_BASE=secret \
-e SECRET_KEY_BASE=<your-secret-key-base> \
-v /var/lib/openproject/pgdata:/var/openproject/pgdata \
-v /var/lib/openproject/assets:/var/openproject/assets \
openproject/openproject:17
@@ -143,7 +143,7 @@ docker run -d -p 8080:80 --name openproject \
Please make sure you set the correct public facing hostname in `OPENPROJECT_HOST__NAME`. If you don't have a load-balancing or proxying web server in front of your docker container,
you will otherwise be vulnerable to [HOST header injections](https://portswigger.net/web-security/host-header), as the internal server has no way of identifying the correct host name. We strongly recommend you use an external load-balancing or proxying web server for termination of TLS/SSL and general security hardening.
**Note**: Make sure to replace `secret` with a random string. One way to generate one is to run `head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 ; echo ''` if you are on Linux.
**Note**: Make sure to replace `<your-secret-key-base>` with a random string. One way to generate one is to run `openssl rand -hex 64`. Store this value securely — it must remain the same across container restarts, otherwise sessions and encrypted database content will be lost.
**Note**: MacOS users might encounter an "Operation not permitted" error on the mounted directories. The fix for this is to create the two directories in a user-owned directory of the host machine.
@@ -474,7 +474,7 @@ The first way is to mount the root certificate via the ```--mount``` option into
```shell
sudo docker run -it -p 8080:80 \
-e SECRET_KEY_BASE=secret \
-e SECRET_KEY_BASE=<your-secret-key-base> \
-e OPENPROJECT_HOST__NAME=localhost:8080 \
-e OPENPROJECT_HTTPS=false \
-e OPENPROJECT_DEFAULT__LANGUAGE=en \
@@ -213,7 +213,7 @@ sudo mv /var/lib/openproject/pgdata-next /var/lib/openproject/pgdata
Finally, you can restart OpenProject with the same command that you used before. For instance:
docker run -d -p 8080:80 --name openproject -e SECRET_KEY_BASE=secret \
docker run -d -p 8080:80 --name openproject -e SECRET_KEY_BASE=<your-secret-key-base> \
-v /var/lib/openproject/pgdata:/var/openproject/pgdata \
-v /var/lib/openproject/assets:/var/openproject/assets \
[...]
@@ -165,7 +165,7 @@ You can now run a new OpenProject container connected to your upgraded PostgreSQ
```bash
docker run -d -p 8080:80 --name openproject \
-e OPENPROJECT_HOST__NAME=openproject.example.com \
-e SECRET_KEY_BASE=secret \
-e SECRET_KEY_BASE=<your-secret-key-base> \
-v /var/lib/openproject/pgdata17:/var/openproject/pgdata \
-v /var/lib/openproject/assets:/var/openproject/assets \
openproject/openproject:17
@@ -59,7 +59,7 @@ If you are using the all-in-one container, then you can simply backup any local
```shell
sudo mkdir -p /var/lib/openproject/{pgdata,assets}
docker run -d -p 8080:80 --name openproject -e SECRET_KEY_BASE=secret \
docker run -d -p 8080:80 --name openproject -e SECRET_KEY_BASE=<your-secret-key-base> \
-v /var/lib/openproject/pgdata:/var/openproject/pgdata \
-v /var/lib/openproject/assets:/var/openproject/assets \
openproject/openproject:17
+103
View File
@@ -0,0 +1,103 @@
# 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.
#++
# Regression guard against the Marshal.load-via-encrypted-cookie RCE chain.
# If anyone flips `cookies_serializer` back to :marshal or :hybrid, the
# 2FA op2fa_remember_token cookie (and any future encrypted cookie) becomes
# a pre-auth RCE primitive as soon as the signing key is known or leaks.
require "spec_helper"
# Canary class used to detect Marshal.load running against attacker-controlled
# cookie payloads. Must be a named top-level constant so Marshal can dump and
# reconstruct it by name. We record the invocation in a class-level flag because
# the cookie jar swallows exceptions raised during deserialization — an
# `expect { … }.not_to raise_error` assertion would not catch a regression.
class CookieSerializerSpecMarshalCanary
@triggered = false
class << self
attr_accessor :triggered
end
def marshal_dump = "canary"
def marshal_load(_)
CookieSerializerSpecMarshalCanary.triggered = true
end
end
RSpec.describe "Cookie serializer" do # rubocop:disable RSpec/DescribeClass
it "is configured to :json (NEVER :marshal or :hybrid)" do
expect(Rails.application.config.action_dispatch.cookies_serializer).to eq(:json)
end
describe "encrypted cookie jar" do
# Forge a cookie whose ciphertext, after authenticated decryption, yields a
# Marshal payload. This mimics either (a) a cookie written by an older
# version of OpenProject when the serializer was :marshal, or (b) an attacker
# who knows SECRET_KEY_BASE (e.g. the historical OVERWRITE_ME for unconfigured
# docker containers) and crafts an exploit payload.
let(:marshal_payload) { Marshal.dump(CookieSerializerSpecMarshalCanary.new) }
let(:forged_cookie) do
salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
cipher = Rails.application.config.action_dispatch.encrypted_cookie_cipher || "aes-256-gcm"
key_len = ActiveSupport::MessageEncryptor.key_len(cipher)
secret = Rails.application.key_generator.generate_key(salt, key_len)
encryptor = ActiveSupport::MessageEncryptor.new(
secret,
cipher: cipher,
serializer: ActiveSupport::MessageEncryptor::NullSerializer
)
encryptor.encrypt_and_sign(marshal_payload, purpose: "cookie.op2fa_remember_token")
end
let(:cookie_jar) do
request = ActionDispatch::TestRequest.create
request.cookies["op2fa_remember_token"] = forged_cookie
ActionDispatch::Cookies::CookieJar.build(request, request.cookies)
end
before { CookieSerializerSpecMarshalCanary.triggered = false }
it "does not invoke Marshal.load on the cookie payload" do
# If the serializer were :marshal/:hybrid, the encrypted cookie jar would
# call Marshal.load on the decrypted payload, reconstructing the canary
# and setting its `triggered` flag. With :json, the JSON deserializer
# rejects the Marshal bytes and the jar returns nil — no Ruby objects
# are revived from attacker-controlled data.
cookie_jar.encrypted["op2fa_remember_token"]
expect(CookieSerializerSpecMarshalCanary.triggered).to be(false),
"Marshal.load was invoked on attacker-controlled cookie payload — " \
"cookies_serializer must be :json, not :marshal or :hybrid."
end
end
end