diff --git a/docker-compose.yml b/docker-compose.yml index b3b153d6679..759afe8ded0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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" diff --git a/lib/open_project.rb b/lib/open_project.rb index 149308044e4..0a098499416 100644 --- a/lib/open_project.rb +++ b/lib/open_project.rb @@ -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, diff --git a/lib/open_project/httpx_ssrf_filter.rb b/lib/open_project/httpx_ssrf_filter.rb new file mode 100644 index 00000000000..be7545c6429 --- /dev/null +++ b/lib/open_project/httpx_ssrf_filter.rb @@ -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 diff --git a/spec/lib/open_project_spec.rb b/spec/lib/open_project_spec.rb new file mode 100644 index 00000000000..22abc49b730 --- /dev/null +++ b/spec/lib/open_project_spec.rb @@ -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