From bcb57911d7c50650aa42a98d42acbbea72a198b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 21 May 2026 15:47:22 +0200 Subject: [PATCH 1/7] Allow serialization of safebuffer without encoding issues When caching a SafeBuffer, it is being returned as a binary string and using concat or similar on something that is cached like that will result in a => # error. we introduce a patch to ensure SafeBuffers are returned as utf-8 instead --- .../patches/message_pack_safe_buffer.rb | 62 ++++++++++++++++ .../message_pack_safe_buffer_fix_spec.rb | 70 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 lib/open_project/patches/message_pack_safe_buffer.rb create mode 100644 spec/lib/open_project/patches/message_pack_safe_buffer_fix_spec.rb diff --git a/lib/open_project/patches/message_pack_safe_buffer.rb b/lib/open_project/patches/message_pack_safe_buffer.rb new file mode 100644 index 00000000000..a5fc942b5fd --- /dev/null +++ b/lib/open_project/patches/message_pack_safe_buffer.rb @@ -0,0 +1,62 @@ +# 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 OpenProject + module Patches + # Rails registers ActiveSupport::SafeBuffer as MessagePack ext type 18 with + # unpacker: :new. ActionView::OutputBuffer (a SafeBuffer subclass) uses "".b + # internally, so the ext payload bytes are BINARY. The default unpacker calls + # SafeBuffer.new(binary_bytes) which reconstructs with ASCII-8BIT encoding, + # causing Encoding::CompatibilityError when the cached HTML is later + # concatenated into a UTF-8 output buffer (e.g. via ActionView concat). + # + # We prepend to MessagePack::Factory so that when Extensions.install registers + # type 18, our override replaces the unpacker with one that force_encodes to + # UTF-8 before constructing the SafeBuffer. + module MessagePackSafeBufferFix + def register_type(type_id, klass = nil, **options, &) + if klass == ActiveSupport::SafeBuffer && options[:unpacker] == :new + options[:unpacker] = ->(bytes) { + retagged = bytes.force_encoding(Encoding::UTF_8) + raise EncodingError, "Cached SafeBuffer payload is not valid UTF-8" unless retagged.valid_encoding? + + ActiveSupport::SafeBuffer.new(retagged) + } + end + + super + end + end + end +end + +OpenProject::Patches.patch_gem_version "activesupport", "8.1.3" do + MessagePack::Factory.prepend OpenProject::Patches::MessagePackSafeBufferFix +end diff --git a/spec/lib/open_project/patches/message_pack_safe_buffer_fix_spec.rb b/spec/lib/open_project/patches/message_pack_safe_buffer_fix_spec.rb new file mode 100644 index 00000000000..91ee3e03810 --- /dev/null +++ b/spec/lib/open_project/patches/message_pack_safe_buffer_fix_spec.rb @@ -0,0 +1,70 @@ +# 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" + +# Rails registers ActiveSupport::SafeBuffer as MessagePack ext type 18 with +# unpacker: :new. MessagePack ext payloads are raw bytes (BINARY), so the default +# unpacker reconstructs SafeBuffer with ASCII-8BIT encoding, even when the +# original was UTF-8. This patch overrides the unpacker to force UTF-8. +RSpec.describe OpenProject::Patches::MessagePackSafeBufferFix do + # Use the same serializer the cache store uses to cover the real path. + let(:serializer) { ActiveSupport::MessagePack::CacheSerializer } + let(:html) { "

Héllo & wörld — «quoted»

" } + + def round_trip(value) + serializer.load(serializer.dump(value)) + end + + shared_examples "a correctly round-tripped SafeBuffer", :aggregate_failures do + it "returns a utf-8 SafeBuffer, preserving the original content and html safety" do + expect(round_trip(subject)).to be_a(ActiveSupport::SafeBuffer) + expect(round_trip(subject).to_s).to eq(subject.to_s.dup.force_encoding(Encoding::UTF_8)) + expect(round_trip(subject).encoding).to eq(Encoding::UTF_8) + expect(round_trip(subject)).to be_html_safe + end + end + + context "with a UTF-8 SafeBuffer (normal render output)" do + subject { ActiveSupport::SafeBuffer.new(html) } + + include_examples "a correctly round-tripped SafeBuffer" + end + + context "with a BINARY-encoded SafeBuffer (e.g. content assembled from binary bytes)" do + subject { ActiveSupport::SafeBuffer.new(html.b) } + + it "has BINARY encoding before round-trip (confirms precondition)" do + expect(subject.encoding).to eq(Encoding::BINARY) + end + + include_examples "a correctly round-tripped SafeBuffer" + end +end From 5caeb4214e62bf7d5e4f8f37d0d7dce9dfbc4677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 22 May 2026 09:26:05 +0200 Subject: [PATCH 2/7] Use upstream patch from rails --- .../patches/message_pack_safe_buffer.rb | 30 +++++++-------- .../message_pack_safe_buffer_fix_spec.rb | 38 ++++++++++--------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/lib/open_project/patches/message_pack_safe_buffer.rb b/lib/open_project/patches/message_pack_safe_buffer.rb index a5fc942b5fd..2656da497d0 100644 --- a/lib/open_project/patches/message_pack_safe_buffer.rb +++ b/lib/open_project/patches/message_pack_safe_buffer.rb @@ -30,25 +30,23 @@ module OpenProject module Patches - # Rails registers ActiveSupport::SafeBuffer as MessagePack ext type 18 with - # unpacker: :new. ActionView::OutputBuffer (a SafeBuffer subclass) uses "".b - # internally, so the ext payload bytes are BINARY. The default unpacker calls - # SafeBuffer.new(binary_bytes) which reconstructs with ASCII-8BIT encoding, - # causing Encoding::CompatibilityError when the cached HTML is later - # concatenated into a UTF-8 output buffer (e.g. via ActionView concat). + # Upstream fix: https://github.com/rails/rails/pull/57429 (merged, not yet released) # - # We prepend to MessagePack::Factory so that when Extensions.install registers - # type 18, our override replaces the unpacker with one that force_encodes to - # UTF-8 before constructing the SafeBuffer. + # Rails registers ActiveSupport::SafeBuffer as MessagePack ext type 18 with + # packer: :to_s, unpacker: :new. Ext payload bytes are always BINARY, so + # SafeBuffer.new(payload) reconstructs with ASCII-8BIT encoding, causing + # Encoding::CompatibilityError when cached HTML is later concatenated into a + # UTF-8 output buffer. + # + # The upstream fix switches to recursive: true with nested packer.write / + # unpacker.read so the MessagePack string codec preserves the original + # encoding across the round-trip. module MessagePackSafeBufferFix def register_type(type_id, klass = nil, **options, &) - if klass == ActiveSupport::SafeBuffer && options[:unpacker] == :new - options[:unpacker] = ->(bytes) { - retagged = bytes.force_encoding(Encoding::UTF_8) - raise EncodingError, "Cached SafeBuffer payload is not valid UTF-8" unless retagged.valid_encoding? - - ActiveSupport::SafeBuffer.new(retagged) - } + if klass == ActiveSupport::SafeBuffer + options[:packer] = ->(buffer, packer) { packer.write(buffer.to_str) } + options[:unpacker] = ->(unpacker) { ActiveSupport::SafeBuffer.new(unpacker.read) } + options[:recursive] = true end super diff --git a/spec/lib/open_project/patches/message_pack_safe_buffer_fix_spec.rb b/spec/lib/open_project/patches/message_pack_safe_buffer_fix_spec.rb index 91ee3e03810..3fc27dd7365 100644 --- a/spec/lib/open_project/patches/message_pack_safe_buffer_fix_spec.rb +++ b/spec/lib/open_project/patches/message_pack_safe_buffer_fix_spec.rb @@ -31,9 +31,11 @@ require "spec_helper" # Rails registers ActiveSupport::SafeBuffer as MessagePack ext type 18 with -# unpacker: :new. MessagePack ext payloads are raw bytes (BINARY), so the default -# unpacker reconstructs SafeBuffer with ASCII-8BIT encoding, even when the -# original was UTF-8. This patch overrides the unpacker to force UTF-8. +# packer: :to_s, unpacker: :new. Ext payloads are raw bytes (BINARY), so the +# default unpacker reconstructs SafeBuffer with ASCII-8BIT encoding even when +# the original was UTF-8. The patch (mirroring rails/rails#57429) switches to +# recursive: true with nested packer.write / unpacker.read so the MessagePack +# string codec preserves the original encoding across the round-trip. RSpec.describe OpenProject::Patches::MessagePackSafeBufferFix do # Use the same serializer the cache store uses to cover the real path. let(:serializer) { ActiveSupport::MessagePack::CacheSerializer } @@ -43,28 +45,28 @@ RSpec.describe OpenProject::Patches::MessagePackSafeBufferFix do serializer.load(serializer.dump(value)) end - shared_examples "a correctly round-tripped SafeBuffer", :aggregate_failures do - it "returns a utf-8 SafeBuffer, preserving the original content and html safety" do - expect(round_trip(subject)).to be_a(ActiveSupport::SafeBuffer) - expect(round_trip(subject).to_s).to eq(subject.to_s.dup.force_encoding(Encoding::UTF_8)) - expect(round_trip(subject).encoding).to eq(Encoding::UTF_8) - expect(round_trip(subject)).to be_html_safe - end - end - context "with a UTF-8 SafeBuffer (normal render output)" do subject { ActiveSupport::SafeBuffer.new(html) } - include_examples "a correctly round-tripped SafeBuffer" + it "roundtrips as a UTF-8 html_safe SafeBuffer and concatenates without error", :aggregate_failures do + result = round_trip(subject) + expect(result).to be_a(ActiveSupport::SafeBuffer) + expect(result).to be_html_safe + expect(result.encoding).to eq(Encoding::UTF_8) + expect(result.to_s).to eq(html) + expect("prefix " + result).to eq("prefix #{html}") + end end - context "with a BINARY-encoded SafeBuffer (e.g. content assembled from binary bytes)" do + context "with a BINARY-encoded SafeBuffer" do subject { ActiveSupport::SafeBuffer.new(html.b) } - it "has BINARY encoding before round-trip (confirms precondition)" do - expect(subject.encoding).to eq(Encoding::BINARY) + it "preserves BINARY encoding across the round-trip", :aggregate_failures do + result = round_trip(subject) + expect(result).to be_a(ActiveSupport::SafeBuffer) + expect(result).to be_html_safe + expect(result.encoding).to eq(Encoding::BINARY) + expect(result.to_str).to eq(html.b) end - - include_examples "a correctly round-tripped SafeBuffer" end end From d9fe91332693bb216782787160713f0c4e8b5124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 8 Jun 2026 15:31:16 +0200 Subject: [PATCH 3/7] Update security fixes --- docs/release-notes/17-3-4/README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/release-notes/17-3-4/README.md diff --git a/docs/release-notes/17-3-4/README.md b/docs/release-notes/17-3-4/README.md new file mode 100644 index 00000000000..0c91883b729 --- /dev/null +++ b/docs/release-notes/17-3-4/README.md @@ -0,0 +1,28 @@ +--- +title: OpenProject 17.3.4 +sidebar_navigation: + title: 17.3.4 +release_version: 17.3.4 +release_date: 2026-06-08 +--- + +# OpenProject 17.3.4 + +Release date: 2026-06-08 + +We released [OpenProject 17.3.4](https://community.openproject.org/versions/2305). +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. + + + + +## Bug fixes and changes + + + + +- Bugfix: Memcached serialization is broken in 17.3.3 \[[#75753](https://community.openproject.org/wp/75753)\] + + + From 3c055198c6ca11d362ea1245fc10df58079dabce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 8 Jun 2026 15:31:17 +0200 Subject: [PATCH 4/7] Add release-notes file --- docs/release-notes/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/release-notes/README.md b/docs/release-notes/README.md index 7e15b7308c4..71a9a34910c 100644 --- a/docs/release-notes/README.md +++ b/docs/release-notes/README.md @@ -13,6 +13,13 @@ Stay up to date and get an overview of the new features included in the releases +## 17.3.4 + +Release date: 2026-06-08 + +[Release Notes](17-3-4/) + + ## 17.3.3 Release date: 2026-06-08 From 04f51891429fef0a610e0d693998106d5f7cff7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 8 Jun 2026 15:31:19 +0200 Subject: [PATCH 5/7] Update hocuspocus image to openproject/hocuspocus:17.3.4 --- docker/prod/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 44bab1d60b9..a1f9bbd6143 100755 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -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.3.3 --chown=$APP_USER:$APP_USER /app /opt/hocuspocus +COPY --from=openproject/hocuspocus:17.3.4 --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 From 3f49de97180e16a929176fa93e1feda91750a843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 8 Jun 2026 15:31:19 +0200 Subject: [PATCH 6/7] Update publiccode.yml --- publiccode.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/publiccode.yml b/publiccode.yml index 27fdcd10f82..c263fddb490 100644 --- a/publiccode.yml +++ b/publiccode.yml @@ -8,7 +8,7 @@ applicationSuite: openDesk url: 'https://github.com/opf/openproject' roadmap: 'https://www.openproject.org/roadmap' releaseDate: '2026-06-08' -softwareVersion: '17.3.3' +softwareVersion: '17.3.4' developmentStatus: stable softwareType: standalone/web logo: 'publiccode_logo.svg' From bd243cfb395288039fa9cf870a3165a15d952830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 8 Jun 2026 15:31:22 +0200 Subject: [PATCH 7/7] Bumped version to 17.3.5 [ci skip] --- lib/open_project/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/open_project/version.rb b/lib/open_project/version.rb index 9dbcb9768d2..3a22b8fec57 100644 --- a/lib/open_project/version.rb +++ b/lib/open_project/version.rb @@ -33,7 +33,7 @@ module OpenProject module VERSION # :nodoc: MAJOR = 17 MINOR = 3 - PATCH = 4 + PATCH = 5 class << self def revision