Conflicts: Gemfile.lockpull/3366/merge
commit
ca1616ae90
@ -0,0 +1,149 @@ |
|||||||
|
#-- copyright |
||||||
|
# OpenProject is a project management system. |
||||||
|
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||||
|
# |
||||||
|
# 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 doc/COPYRIGHT.rdoc 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.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 |
||||||
|
URI.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 |
@ -0,0 +1,126 @@ |
|||||||
|
#-- copyright |
||||||
|
# OpenProject is a project management system. |
||||||
|
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||||
|
# |
||||||
|
# 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 doc/COPYRIGHT.rdoc for more details. |
||||||
|
#++ |
||||||
|
|
||||||
|
require File.expand_path('../../spec_helper', __FILE__) |
||||||
|
|
||||||
|
describe RedirectPolicy, type: :controller do |
||||||
|
let(:host) { 'test.host' } |
||||||
|
|
||||||
|
let(:return_escaped) { true } |
||||||
|
let(:default) { 'http://test.foo/default' } |
||||||
|
|
||||||
|
let(:policy) { |
||||||
|
described_class.new( |
||||||
|
back_url, |
||||||
|
default: default, |
||||||
|
hostname: host, |
||||||
|
return_escaped: return_escaped |
||||||
|
) |
||||||
|
} |
||||||
|
let(:subject) { policy.redirect_url } |
||||||
|
|
||||||
|
shared_examples 'redirects to default' do |url| |
||||||
|
let(:back_url) { url } |
||||||
|
it "#{url} redirects to the default URL" do |
||||||
|
expect(subject).to eq(default) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
shared_examples 'valid redirect URL' do |url| |
||||||
|
let(:back_url) { url } |
||||||
|
it "#{url} is valid" do |
||||||
|
expect(subject).to eq(url) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
shared_examples 'valid redirect, escaped URL' do |input, output| |
||||||
|
let(:back_url) { input } |
||||||
|
it "#{input} is valid, but escaped to #{output}" do |
||||||
|
expect(subject).to eq(output) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
shared_examples 'ignores invalid URLs' do |
||||||
|
uris = %w( |
||||||
|
//test.foo/fake |
||||||
|
//bar@test.foo |
||||||
|
//test.foo |
||||||
|
////test.foo |
||||||
|
@test.foo |
||||||
|
fake@test.foo |
||||||
|
//foo:bar@test.foo |
||||||
|
/../somedir |
||||||
|
/work_packages/../../secret |
||||||
|
) |
||||||
|
|
||||||
|
uris.each do |uri| |
||||||
|
it_behaves_like 'redirects to default', uri |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
it_behaves_like 'ignores invalid URLs' |
||||||
|
it_behaves_like 'valid redirect URL', '/work_packages/1234?filter=[foo,bar]' |
||||||
|
|
||||||
|
it_behaves_like 'valid redirect, escaped URL', |
||||||
|
'http://test.host/?a=\11\15', |
||||||
|
'http://test.host/?a=%5C11%5C15' |
||||||
|
|
||||||
|
context 'without escaped return URLs' do |
||||||
|
let(:return_escaped) { false } |
||||||
|
it_behaves_like 'valid redirect URL', '/work_packages/1234?filter=[foo,bar]' |
||||||
|
it_behaves_like 'valid redirect URL', 'http://test.host/?a=\11\15' |
||||||
|
end |
||||||
|
|
||||||
|
context 'with relative root' do |
||||||
|
let(:relative_root) { '/mysubdir' } |
||||||
|
|
||||||
|
before do |
||||||
|
allow(OpenProject::Configuration) |
||||||
|
.to receive(:[]).with('rails_relative_url_root') |
||||||
|
.and_return(relative_root) |
||||||
|
end |
||||||
|
|
||||||
|
it_behaves_like 'valid redirect URL', '/mysubdir/work_packages/1234' |
||||||
|
it_behaves_like 'valid redirect URL', '/mysubdir' |
||||||
|
it_behaves_like 'redirects to default', '/' |
||||||
|
it_behaves_like 'redirects to default', '/foobar' |
||||||
|
it_behaves_like 'redirects to default', '/mysubdir/../foobar' |
||||||
|
it_behaves_like 'redirects to default', '/mysubdir/%2E%2E/secret/etc/passwd' |
||||||
|
it_behaves_like 'redirects to default', '/%2E%2E/secret/etc/passwd' |
||||||
|
it_behaves_like 'redirects to default', '/foobar/%2E%2E/secret/etc/passwd' |
||||||
|
it_behaves_like 'redirects to default', 'wusdus/%2E%2E/%2E%2E/secret/etc/passwd' |
||||||
|
end |
||||||
|
|
||||||
|
describe 'auth credentials' do |
||||||
|
let(:back_url) { 'http://user:pass@test.host/work_packages/123' } |
||||||
|
|
||||||
|
it 'removes the credentials' do |
||||||
|
expect(subject).to eq('http://test.host/work_packages/123') |
||||||
|
end |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue