This commit is contained in:
Markus Kahl
2026-03-05 13:36:32 +00:00
parent 7afb5f92ef
commit 2a56b3beea
6 changed files with 51 additions and 18 deletions
+1 -1
View File
@@ -98,7 +98,7 @@ class AdminController < ApplicationController
def validate_email_settings
smtp_addr = ActionMailer::Base.smtp_settings[:address]
if !OpenProject::SsrfProtection.safe_ip_address(smtp_addr)
if !OpenProject::SsrfProtection.safe_ip(smtp_addr)
flash[:error] = I18n.t :notice_smtp_address_unsafe, address: smtp_addr
redirect_to admin_settings_mail_notifications_path, status: :see_other
+3 -2
View File
@@ -1181,9 +1181,10 @@ module Settings
},
ssrf_protection_ip_allowlist: {
description: "
Connections to certain IP addresses are blocked to prevent SSRF attacks.
Connections to certain IP addresses (such as private ranges) are blocked to prevent SSRF attacks.
Use this setting to explicitly allow given IP addresses which would otherwise be blocked.
Takes a comma or space separated list of IPv4 and IPv6 addresses (including masks for ranges), e.g. `192.168.255.255/16`.
Takes a comma or space separated list of IPv4 and IPv6 addresses (including masks for ranges),
e.g. `192.168.255.255/16`.
".squish,
format: :string,
default: "",
+37 -7
View File
@@ -1,3 +1,33 @@
# 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
class SsrfProtection < ::SsrfFilter
class << self
@@ -10,23 +40,23 @@ module OpenProject
#
# @param hostname_or_ip_address [String] The hostname (e.g. localhost) or IP address (e.g. 127.0.0.1) to check
# @return [IPAddr] The first safe IP address which can be used for a request, or `nil` if there aren't any
def safe_ip_address(hostname_or_ip_address)
def safe_ip(hostname_or_ip_address)
if hostname_or_ip_address.is_a? IPAddr
safe_ip_address? hostname_or_ip_address
safe_ip_address hostname_or_ip_address
elsif [Resolv::IPv4::Regex, Resolv::IPv6::Regex].any? { |regex| hostname_or_ip_address =~ regex }
safe_ip_address? IPAddr.new(hostname_or_ip_address)
safe_ip_address IPAddr.new(hostname_or_ip_address)
else
safe_hostname? hostname_or_ip_address
safe_ip_address_for_hostname hostname_or_ip_address
end
end
def safe_hostname?(hostname)
def safe_ip_address_for_hostname(hostname)
ip_addresses = resolver.call hostname
ip_addresses.find { |addr| safe_ip_address? addr }
ip_addresses.find { |addr| safe_ip_address addr }
end
def safe_ip_address?(ip_address)
def safe_ip_address(ip_address)
ip_address if !unsafe_ip_address?(ip_address) || allowed_ip_address?(ip_address)
end
+1 -1
View File
@@ -145,7 +145,7 @@ RSpec.describe AdminController do
end
end
context "with an unsafe SMTP adress on the allowlist", with_ssrf_ip_allowlist: ['127.0.0.1'] do
context "with an unsafe SMTP adress on the allowlist", with_ssrf_ip_allowlist: %w(127.0.0.1) do
before do
allow(ActionMailer::Base).to receive(:smtp_settings).and_return({ address: "127.0.0.1" })
@@ -38,7 +38,7 @@ RSpec.describe "Test mail notification", :js do
visit admin_settings_mail_notifications_path(tab: :notifications)
end
it "shows the correct message on errors in test notification (Regression #28226)", with_ssrf_ip_allowlist: ['127.0.0.1'] do
it "shows the correct message on errors in test notification (Regression #28226)", with_ssrf_ip_allowlist: %w(127.0.0.1) do
error_message = '"error" with <strong>Markup?</strong>'
expect(UserMailer).to receive(:test_mail).with(admin)
.and_raise error_message
@@ -1,3 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -29,8 +31,8 @@
require "spec_helper"
RSpec.describe OpenProject::SsrfProtection do
describe ".safe_ip_address" do
subject { described_class.safe_ip_address(input) }
describe ".safe_ip" do
subject { described_class.safe_ip(input) }
context "with a public IPv4 string" do
let(:input) { "1.1.1.1" }
@@ -92,10 +94,10 @@ RSpec.describe OpenProject::SsrfProtection do
let(:input) { "example.com" }
before do
allow(described_class).to receive(:resolver).and_return(->(host) { resolved_addresses })
allow(described_class).to receive(:resolver).and_return(proc { resolved_addresses })
end
context "that resolves to a public IP" do
context "if it resolves to a public IP" do
let(:resolved_addresses) { [IPAddr.new("93.184.216.34")] }
it "returns the resolved IPAddr" do
@@ -103,7 +105,7 @@ RSpec.describe OpenProject::SsrfProtection do
end
end
context "that resolves to a private IP" do
context "if it resolves to a private IP" do
let(:resolved_addresses) { [IPAddr.new("10.0.0.1")] }
it "returns nil" do
@@ -111,7 +113,7 @@ RSpec.describe OpenProject::SsrfProtection do
end
end
context "that resolves to multiple IPs with both private and public" do
context "if it resolves to multiple IPs with both private and public" do
let(:resolved_addresses) { [IPAddr.new("10.0.0.1"), IPAddr.new("1.2.3.4")] }
it "returns the first public IPAddr" do