Skip to content

Commit e97aef4

Browse files
authored
Merge pull request #75 from basecamp/flavorjones/handle-invalid-paths
Gracefully handle more invalid paths
2 parents e206b0a + c1b9c4d commit e97aef4

3 files changed

Lines changed: 89 additions & 8 deletions

File tree

lib/google_sign_in/redirect_protector.rb

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,27 @@ class Violation < StandardError; end
99
QUALIFIED_URL_PATTERN = /\A#{URI::DEFAULT_PARSER.make_regexp}\z/
1010

1111
def ensure_same_origin(target, source)
12-
if (target =~ QUALIFIED_URL_PATTERN && origin_of(target) == origin_of(source)) ||
13-
target =~ URI::DEFAULT_PARSER.regexp[:ABS_PATH]
14-
return
12+
unless uri_same_origin?(target, source) || absolute_path?(target)
13+
raise Violation, "Redirect target #{target.inspect} does not have same origin as request #{source.inspect}"
1514
end
16-
17-
raise Violation, "Redirect target #{target.inspect} does not have same origin as request (expected #{origin_of(source)})"
1815
end
1916

2017
private
18+
def uri_same_origin?(target, source)
19+
target =~ QUALIFIED_URL_PATTERN && origin_of(target) == origin_of(source)
20+
rescue ArgumentError, URI::Error
21+
false
22+
end
23+
24+
def absolute_path?(target)
25+
target =~ URI::DEFAULT_PARSER.regexp[:ABS_PATH] && URI(target).host.nil? && !target.start_with?("//")
26+
rescue ArgumentError, URI::Error
27+
false
28+
end
29+
2130
def origin_of(url)
2231
uri = URI(url)
2332
"#{uri.scheme}://#{uri.host}:#{uri.port}"
24-
rescue ArgumentError
25-
nil
2633
end
2734
end
2835
end

test/controllers/callbacks_controller_test.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,48 @@ class GoogleSignIn::CallbacksControllerTest < ActionDispatch::IntegrationTest
103103
assert_response :bad_request
104104
end
105105

106+
test "protecting against open redirects given a malformed URI" do
107+
post google_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com\n\r@\n\revil.example.org/login' }
108+
assert_response :redirect
109+
110+
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
111+
assert_response :bad_request
112+
end
113+
114+
test "rejects proceed_to paths if they are relative" do
115+
post google_sign_in.authorization_url, params: { proceed_to: 'login' }
116+
assert_response :redirect
117+
118+
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
119+
assert_response :bad_request
120+
end
121+
122+
test "accepts proceed_to paths if they are absolute" do
123+
post google_sign_in.authorization_url, params: { proceed_to: '/login' }
124+
assert_response :redirect
125+
126+
stub_token_for '4/SgCpHSVW5-Cy', access_token: 'ya29.GlwIBo', id_token: 'eyJhbGciOiJSUzI'
127+
128+
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
129+
assert_redirected_to 'http://www.example.com/login'
130+
end
131+
132+
test "protecting against open redirects given a double-slash net path" do
133+
post google_sign_in.authorization_url, params: { proceed_to: '//evil.example.org' }
134+
assert_response :redirect
135+
136+
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
137+
assert_response :bad_request
138+
end
139+
140+
test "protecting against open redirects given a triple-slash net path" do
141+
post google_sign_in.authorization_url, params: { proceed_to: '///evil.example.org' }
142+
assert_response :redirect
143+
144+
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
145+
assert_response :bad_request
146+
end
147+
106148
test "receiving no proceed_to URL" do
107149
get google_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: 'invalid')
108150
assert_response :bad_request

test/models/redirect_protector_test.rb

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,41 @@ class GoogleSignIn::RedirectProtectorTest < ActiveSupport::TestCase
4444
end
4545
end
4646

47-
test "allows path target" do
47+
test "disallows relative path target" do
48+
assert_raises GoogleSignIn::RedirectProtector::Violation do
49+
GoogleSignIn::RedirectProtector.ensure_same_origin 'callback', 'https://basecamp.com'
50+
end
51+
end
52+
53+
test "allows absolute path target" do
4854
assert_nothing_raised do
4955
GoogleSignIn::RedirectProtector.ensure_same_origin '/callback', 'https://basecamp.com'
5056
end
5157
end
58+
59+
test "disallows double-slash path target" do
60+
assert_raises GoogleSignIn::RedirectProtector::Violation do
61+
GoogleSignIn::RedirectProtector.ensure_same_origin '//evil.example.org', 'https://basecamp.com'
62+
end
63+
end
64+
65+
test "disallows triple-slash path target" do
66+
assert_raises GoogleSignIn::RedirectProtector::Violation do
67+
GoogleSignIn::RedirectProtector.ensure_same_origin '///evil.example.org', 'https://basecamp.com'
68+
end
69+
end
70+
71+
test "disallows invalid paths" do
72+
assert_raises GoogleSignIn::RedirectProtector::Violation do
73+
GoogleSignIn::RedirectProtector.ensure_same_origin '/a path with spaces is invalid', 'https://basecamp.com'
74+
end
75+
76+
assert_raises GoogleSignIn::RedirectProtector::Violation do
77+
GoogleSignIn::RedirectProtector.ensure_same_origin '/path#with-fragment', 'https://basecamp.com'
78+
end
79+
80+
assert_raises GoogleSignIn::RedirectProtector::Violation do
81+
GoogleSignIn::RedirectProtector.ensure_same_origin '/path?with=query', 'https://basecamp.com'
82+
end
83+
end
5284
end

0 commit comments

Comments
 (0)