OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
openproject/app/policies/redirect_policy.rb

149 lines
4.6 KiB

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 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 'uri'
require 'cgi'
# This capsulates the validation of a requested redirect URL.
#
class RedirectPolicy
attr_reader :validated_redirect_url, :request
def initialize(requested_url, hostname:, default:, return_escaped: true)
@current_host = hostname
@return_escaped = return_escaped
@requested_url = preprocess(requested_url)
@default_url = default
end
##
# Performs all validations for the requested URL
def valid?
return false if @requested_url.nil?
[
# back_url must not contain two consecutive dots
:no_upper_levels,
# Require the path to begin with a slash
:path_has_slash,
# do not redirect user to another host
:same_host,
# do not redirect user to the login or register page
:path_not_blacklisted,
# do not redirect to another subdirectory
:matches_relative_root
].all? { |check| send(check) }
end
##
# Return a valid redirect URI.
# If the validation check on the current back URL apply
def redirect_url
if valid?
postprocess(@requested_url)
else
@default_url
end
end
private
##
# Preprocesses the requested redirect URL.
# - Escapes it when necessary
# - Tries to parse it
# - Escapes the redirect URL when requested so.
def preprocess(requested)
url = URI::RFC2396_Parser.new.escape(CGI.unescape(requested.to_s))
URI.parse(url)
rescue URI::InvalidURIError => e
Rails.logger.warn("Encountered invalid redirect URL '#{requested}': #{e.message}")
nil
end
##
# Postprocesses the validated URL
def postprocess(redirect_url)
# Remove basic auth credentials
redirect_url.userinfo = ''
if @return_escaped
redirect_url.to_s
else
CGI.unescape(redirect_url.to_s)
end
end
##
# Avoid paths with references to parent paths
def no_upper_levels
!@requested_url.path.include? '../'
end
##
# Require URLs to contain a path slash.
# This will always be the case for parsed URLs unless
# +URI.parse('@foo.bar')+ or a non-root relative URL +URI.parse('foo')+
def path_has_slash
@requested_url.path =~ %r{\A/([^/]|\z)}
end
##
# do not redirect user to another host (even protocol relative urls have the host set)
# whenever a host is set it must match the request's host
def same_host
@requested_url.host.nil? || @requested_url.host == @current_host
end
##
# Avoid redirect URLs to specific locations, such as login page
def path_not_blacklisted
!@requested_url.path.match(
%r{/(
# Ignore login since redirect to back url is result of successful login.
login |
# When signing out with a direct login provider enabled you will be left at the logout
# page with a message indicating that you were logged out. Logging in from there would
# normally cause you to be redirected to this page. As it is the logout page, however,
# this would log you right out again after a successful login.
logout |
# Avoid sending users to the register form. The exact reasoning behind
# this is unclear, but grown from tradition.
account/register
)}x # ignore whitespace
)
end
##
# Requires the redirect URL to reside inside the relative root, when given.
def matches_relative_root
relative_root = OpenProject::Configuration['rails_relative_url_root']
relative_root.blank? || @requested_url.path.starts_with?(relative_root)
end
end