Add SSRF filter for HTTPX

Filtering in front of HTTPX calls is less secure, because it's vulnerable to
DNS rebinding. In addition to that it's also duplicate work, because all affected
callsites would have to make sure to "remember" SSRF filtering.

This SSRF filter is inspired by the original HTTPX SSRF Filter, but using our custom
IP address matcher that allows to configure safe IP addresses or ranges.
This commit is contained in:
Jan Sandbrink
2026-06-01 10:57:00 +02:00
parent 306173ad3f
commit 294611cc59
4 changed files with 137 additions and 3 deletions
+4 -3
View File
@@ -36,17 +36,18 @@ x-op-backend: &backend
target: develop target: develop
<<: [*image, *restart_policy] <<: [*image, *restart_policy]
environment: 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}" 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_CACHE__MEMCACHE__SERVER: cache:11211
OPENPROJECT_EDITION: ${OPENPROJECT_EDITION:-standard}
OPENPROJECT_RAILS__CACHE__STORE: file_store OPENPROJECT_RAILS__CACHE__STORE: file_store
OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}" 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_SSRF_PROTECTION_IP_ALLOWLIST: 0.0.0.0/0,::0/0 # disabling SSRF in dev to not interfere with local integrations (Nextcloud etc.)
OPENPROJECT_EDITION: ${OPENPROJECT_EDITION:-standard}
OPENPROJECT_WEB_MAX__THREADS: 1 OPENPROJECT_WEB_MAX__THREADS: 1
OPENPROJECT_WEB_MIN__THREADS: 1 OPENPROJECT_WEB_MIN__THREADS: 1
OPENPROJECT_WEB_WORKERS: 0 OPENPROJECT_WEB_WORKERS: 0
PIDFILE: /home/dev/openproject/tmpfs/pids/server.pid PIDFILE: /home/dev/openproject/tmpfs/pids/server.pid
RAILS_ENV: development
volumes: volumes:
- ".:/home/dev/openproject" - ".:/home/dev/openproject"
- "opdata:/var/openproject/assets" - "opdata:/var/openproject/assets"
+2
View File
@@ -34,6 +34,7 @@ require "open_project/patches"
require "open_project/mime_type" require "open_project/mime_type"
require "open_project/custom_styles/design" require "open_project/custom_styles/design"
require "open_project/httpx_appsignal" require "open_project/httpx_appsignal"
require "open_project/httpx_ssrf_filter"
require "redmine/plugin" require "redmine/plugin"
require "csv" require "csv"
@@ -62,6 +63,7 @@ module OpenProject
.with(headers: { "User-Agent" => "OpenProject #{OpenProject::VERSION.to_semver} HTTPX Client" }) .with(headers: { "User-Agent" => "OpenProject #{OpenProject::VERSION.to_semver} HTTPX Client" })
.plugin(:auth) .plugin(:auth)
.plugin(:webdav) .plugin(:webdav)
.plugin(HttpxSsrfFilter)
.with( .with(
timeout: { timeout: {
connect_timeout: OpenProject::Configuration.httpx_connect_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