Merge pull request #23480 from opf/httpx-ssrf-filter

Add SSRF filter for HTTPX
This commit is contained in:
Jan Sandbrink
2026-06-04 07:19:42 +02:00
committed by GitHub
4 changed files with 137 additions and 3 deletions
+4 -3
View File
@@ -36,17 +36,18 @@ x-op-backend: &backend
target: develop
<<: [*image, *restart_policy]
environment:
DATABASE_URL: postgresql://${DB_USERNAME:-postgres}:${DB_PASSWORD:-postgres}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_DATABASE:-openproject}?pool=100
LOCAL_DEV_CHECK: "${LOCAL_DEV_CHECK:?The docker-compose file for OpenProject has moved to https://github.com/opf/openproject-docker-compose}"
RAILS_ENV: development
OPENPROJECT_CACHE__MEMCACHE__SERVER: cache:11211
OPENPROJECT_EDITION: ${OPENPROJECT_EDITION:-standard}
OPENPROJECT_RAILS__CACHE__STORE: file_store
OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}"
DATABASE_URL: postgresql://${DB_USERNAME:-postgres}:${DB_PASSWORD:-postgres}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_DATABASE:-openproject}?pool=100
OPENPROJECT_EDITION: ${OPENPROJECT_EDITION:-standard}
OPENPROJECT_SSRF_PROTECTION_IP_ALLOWLIST: 0.0.0.0/0,::0/0 # disabling SSRF in dev to not interfere with local integrations (Nextcloud etc.)
OPENPROJECT_WEB_MAX__THREADS: 1
OPENPROJECT_WEB_MIN__THREADS: 1
OPENPROJECT_WEB_WORKERS: 0
PIDFILE: /home/dev/openproject/tmpfs/pids/server.pid
RAILS_ENV: development
volumes:
- ".:/home/dev/openproject"
- "opdata:/var/openproject/assets"
+2
View File
@@ -34,6 +34,7 @@ require "open_project/patches"
require "open_project/mime_type"
require "open_project/custom_styles/design"
require "open_project/httpx_appsignal"
require "open_project/httpx_ssrf_filter"
require "redmine/plugin"
require "csv"
@@ -62,6 +63,7 @@ module OpenProject
.with(headers: { "User-Agent" => "OpenProject #{OpenProject::VERSION.to_semver} HTTPX Client" })
.plugin(:auth)
.plugin(:webdav)
.plugin(HttpxSsrfFilter)
.with(
timeout: {
connect_timeout: OpenProject::Configuration.httpx_connect_timeout,
+57
View File
@@ -0,0 +1,57 @@
# 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
# An SSRF filter for HTTPX based on the original plugin.
# See https://gitlab.com/os85/httpx/-/blob/master/lib/httpx/plugins/ssrf_filter.rb
#
# The main difference is that we use our own subclass of `SsrfFilter` to perform the matching of unsafe IP addresses.
# We are thus consulting our own allow list of IP addresses before blocking an IP address.
module HttpxSsrfFilter
class ServerSideRequestForgeryError < HTTPX::Error; end
module ConnectionMethods
def initialize(*)
super
rescue ServerSideRequestForgeryError => e
# may raise when IPs are passed as options via :addresses
throw(:resolve_error, e)
end
def addresses=(addrs)
addrs.reject!(&SsrfProtection.method(:unsafe_ip_address?)) # rubocop:disable Performance/MethodObjectAsBlock
raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?
super
end
end
end
end
+74
View File
@@ -0,0 +1,74 @@
# 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 OpenProject do
describe ".httpx" do
subject(:httpx) { described_class.httpx }
let(:public_endpoint) { "https://openproject.org/rspec-test" }
let(:private_endpoint) { "https://localhost/rspec-test" }
it "sets a proper User-Agent header", :webmock do
stub_request(:get, public_endpoint).to_return(status: 204)
httpx.get(public_endpoint)
expect(WebMock).to have_requested(:get, public_endpoint)
.with(headers: { "User-Agent": /OpenProject \d+\.\d+\.\d+ HTTPX Client/ })
end
# We can't use webmock for these tests, as it would interfere too early and thus we couldn't test
# whether corresponding requests would've been made
describe "SSRF filtering" do
it "includes SSRF filtering for private IP addresses" do
result = httpx.get(private_endpoint)
expect(result.error).to be_a(OpenProject::HttpxSsrfFilter::ServerSideRequestForgeryError)
end
it "does not filter requests to public IP addresses" do
result = httpx.get(public_endpoint)
expect(result.error).to be_nil
end
context "when local IP addresses are allowed" do
before do
allow(OpenProject::Configuration).to receive(:ssrf_protection_ip_allowlist)
.and_return([IPAddr.new("127.0.0.1"), IPAddr.new("::1")])
end
it "does not filter local requests" do
result = httpx.get(private_endpoint)
expect(result.error).not_to be_a(OpenProject::HttpxSsrfFilter::ServerSideRequestForgeryError)
end
end
end
end
end