From 40301c346305bdd60fbc5f1cf2ecbd866427cf6c Mon Sep 17 00:00:00 2001 From: Jan Sandbrink Date: Thu, 4 Jun 2026 13:14:51 +0200 Subject: [PATCH 1/3] Make SSRF error message more specific Feedback from devs that were confronted with the "is not an allowed host" message shows, that the message is not very actionable. It's not clear why something that is clearly a legitimate and existing host would be considered "not allowed". The new error message clearly points at the SSRF policy as the source. Making the problem more search engine friendly and hopefully allowing admins to better understand what they have to fix. --- config/locales/en.yml | 2 +- .../app/services/openid_connect/providers/update_service.rb | 2 +- .../services/openid_connect/providers/update_service_spec.rb | 2 +- .../app/validator/nextcloud_compatible_host_validator.rb | 2 +- .../contracts/storages/storages/shared_contract_examples.rb | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 63344b1fc29..a8b9cb507d1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2245,7 +2245,6 @@ en: not_a_datetime: "is not a valid date time." not_a_number: "is not a number." not_allowed: "is invalid because of missing permissions." - host_not_allowed: "is not an allowed host." not_json: "is not parseable as JSON." not_json_object: "is not a JSON object." not_an_integer: "is not an integer." @@ -2258,6 +2257,7 @@ en: regex_list_invalid: "Lines %{invalid_lines} could not be parsed as regular expression." hexcode_invalid: "is not a valid 6-digit hexadecimal color code." smaller_than_or_equal_to_max_length: "must be smaller than or equal to maximum length." + ssrf_filtered: "violates the SSRF policy of this OpenProject instance." taken: "has already been taken." too_long: "is too long (maximum is %{count} characters)." too_short: "is too short (minimum is %{count} characters)." diff --git a/modules/openid_connect/app/services/openid_connect/providers/update_service.rb b/modules/openid_connect/app/services/openid_connect/providers/update_service.rb index 1c4ab6167d4..e29d05de295 100644 --- a/modules/openid_connect/app/services/openid_connect/providers/update_service.rb +++ b/modules/openid_connect/app/services/openid_connect/providers/update_service.rb @@ -114,7 +114,7 @@ module OpenIDConnect if host.present? && OpenProject::SsrfProtection.safe_ip?(host) true else - call.errors.add(:metadata_url, :host_not_allowed) + call.errors.add(:metadata_url, :ssrf_filtered) call.success = false false end diff --git a/modules/openid_connect/spec/services/openid_connect/providers/update_service_spec.rb b/modules/openid_connect/spec/services/openid_connect/providers/update_service_spec.rb index 8ce703e4ae3..7be98aad37f 100644 --- a/modules/openid_connect/spec/services/openid_connect/providers/update_service_spec.rb +++ b/modules/openid_connect/spec/services/openid_connect/providers/update_service_spec.rb @@ -92,7 +92,7 @@ RSpec.describe OpenIDConnect::Providers::UpdateService, type: :model do result = service_call expect(result).not_to be_success - expect(result.errors[:metadata_url]).to include("is not an allowed host.") + expect(result.errors[:metadata_url]).to include("violates the SSRF policy of this OpenProject instance.") expect(httpx_session).not_to have_received(:get) end end diff --git a/modules/storages/app/validator/nextcloud_compatible_host_validator.rb b/modules/storages/app/validator/nextcloud_compatible_host_validator.rb index aabdcbcf612..ef4760e1a38 100644 --- a/modules/storages/app/validator/nextcloud_compatible_host_validator.rb +++ b/modules/storages/app/validator/nextcloud_compatible_host_validator.rb @@ -50,7 +50,7 @@ class NextcloudCompatibleHostValidator < ActiveModel::EachValidator return false if host.blank? return true if OpenProject::SsrfProtection.safe_ip?(host) - contract.errors.add(attribute, :host_not_allowed) + contract.errors.add(attribute, :ssrf_filtered) false rescue URI::InvalidURIError false diff --git a/modules/storages/spec/contracts/storages/storages/shared_contract_examples.rb b/modules/storages/spec/contracts/storages/storages/shared_contract_examples.rb index 0b1f83ae507..d33470a736b 100644 --- a/modules/storages/spec/contracts/storages/storages/shared_contract_examples.rb +++ b/modules/storages/spec/contracts/storages/storages/shared_contract_examples.rb @@ -275,7 +275,7 @@ RSpec.shared_examples_for "nextcloud storage contract", :storage_server_helpers, context "when host is localhost" do let(:storage_host) { "http://localhost:1234" } - include_examples "contract is invalid", host: :host_not_allowed + include_examples "contract is invalid", host: :ssrf_filtered it "does not perform metadata discovery requests" do contract.validate @@ -288,7 +288,7 @@ RSpec.shared_examples_for "nextcloud storage contract", :storage_server_helpers, context "when host uses https protocol" do let(:storage_host) { "https://172.16.193.146" } - include_examples "contract is invalid", host: :host_not_allowed + include_examples "contract is invalid", host: :ssrf_filtered end end From b4ba7ac8c099be2f6d3b169363bb9ad07f2044a2 Mon Sep 17 00:00:00 2001 From: Jan Sandbrink Date: Thu, 4 Jun 2026 13:16:56 +0200 Subject: [PATCH 2/3] Include SSRF hint in release notes Our changes to SSRF filtering (notably: applying it everywhere) can easily affect running instances of OpenProject. Including this hint in the release notes hopefully helps admins deploying their own instances to be aware of the upcoming change. --- docs/release-notes/17-6-0/README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/17-6-0/README.md b/docs/release-notes/17-6-0/README.md index 7fe7625d46d..c08b6b3ce9e 100644 --- a/docs/release-notes/17-6-0/README.md +++ b/docs/release-notes/17-6-0/README.md @@ -19,7 +19,20 @@ In these Release Notes, we will give an overview of important feature changes. A ## Important updates and breaking changes - +### Integrations (e.g. Nextcloud and XWiki) respect global SSRF filters + +To increase the security of OpenProject installations, we've added protections against server-side request forgery in previous releases +of OpenProject. These prevent OpenProject from making network requests into private IP address space. + +Starting with OpenProject 17.6, these protections expand into the code that's responsible for web requests of storage and wiki integrations as well. +This means if you have a Nextcloud instance or an XWiki instance reachable via a private (i.e. not publicly routable) IP address, you need to +add it to the SSRF allowlist to be able to keep the integration working. This is usually achieved by defining the following environment variable: + +``` +OPENPROJECT_SSRF_PROTECTION_IP_ALLOWLIST=2001:db8:100::/48 +``` + +The list accepts one or multiple IP addresses or ranges (in CIDR notation) that shall be exempt from SSRF filtering. From 07372e35140ede00ef61006aef0a1fa8ec9e93f3 Mon Sep 17 00:00:00 2001 From: Jan Sandbrink Date: Thu, 4 Jun 2026 13:28:02 +0200 Subject: [PATCH 3/3] Try to order some YAML keys --- config/locales/en.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index a8b9cb507d1..0dfcf46bfcc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2197,17 +2197,14 @@ en: before: "must be before %{date}." before_or_equal_to: "must be before or equal to %{date}." blank: "can't be blank." - not_before_start_date: "must not be before the start date." - overlapping_range: "overlaps with an existing non-working day range." blank_nested: "needs to have the property '%{property}' set." cannot_delete_mapping: "is required. Cannot be deleted." - is_for_all_cannot_modify: "is for all projects and can therefore not be modified." cant_link_a_work_package_with_a_descendant: "A work package cannot be linked to one of its subtasks." circular_dependency: "This relation would create a circular dependency." confirmation: "doesn't match %{attribute}." could_not_be_copied: "%{dependency} could not be (fully) copied." + datetime_must_be_in_future: "must be in the future." does_not_exist: "does not exist." - user_already_in_department: "User %{user_id} is already a member of department %{department_id}." error_enterprise_only: "%{action} is only available in the OpenProject Enterprise edition." error_unauthorized: "may not be accessed." error_readonly: "was attempted to be written but is not writable." @@ -2228,18 +2225,20 @@ en: greater_than_or_equal_to: "must be greater than or equal to %{count}." greater_than_or_equal_to_start_date: "must be greater than or equal to the start date." greater_than_start_date: "must be greater than the start date." + hexcode_invalid: "is not a valid 6-digit hexadecimal color code." inclusion: "is not set to one of the allowed values." inclusion_nested: "is not set to one of the allowed values at path '%{path}'." invalid: "is invalid." invalid_uri: "must be a valid URI." invalid_url: "is not a valid URL." invalid_url_scheme: "is not a supported protocol (allowed: %{allowed_schemes})." + is_for_all_cannot_modify: "is for all projects and can therefore not be modified." less_than_or_equal_to: "must be less than or equal to %{count}." not_available: "is not available due to a system configuration." + not_before_start_date: "must not be before the start date." not_deletable: "cannot be deleted." not_editable: "cannot be edited because it is already in effect." not_current_user: "is not the current user." - system_wide_non_working_day_exists: "conflicts with an existing system-wide non-working day for this date." not_found: "not found." not_a_date: "is not a valid date." not_a_datetime: "is not a valid date time." @@ -2250,14 +2249,14 @@ en: not_an_integer: "is not an integer." not_an_iso_date: "is not a valid date. Required format: YYYY-MM-DD." not_same_project: "doesn't belong to the same project." - datetime_must_be_in_future: "must be in the future." odd: "must be odd." + overlapping_range: "overlaps with an existing non-working day range." regex_match_failed: "does not match the regular expression %{expression}." regex_invalid: "could not be validated with the associated regular expression." regex_list_invalid: "Lines %{invalid_lines} could not be parsed as regular expression." - hexcode_invalid: "is not a valid 6-digit hexadecimal color code." smaller_than_or_equal_to_max_length: "must be smaller than or equal to maximum length." ssrf_filtered: "violates the SSRF policy of this OpenProject instance." + system_wide_non_working_day_exists: "conflicts with an existing system-wide non-working day for this date." taken: "has already been taken." too_long: "is too long (maximum is %{count} characters)." too_short: "is too short (minimum is %{count} characters)." @@ -2269,6 +2268,7 @@ en: unremovable: "cannot be removed." url_not_secure_context: > is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. + user_already_in_department: "User %{user_id} is already a member of department %{department_id}." wrong_length: "is the wrong length (should be %{count} characters)." models: group: