Skip to content

Commit 8eec97b

Browse files
Merge pull request #14 from gerardo-navarro/gerardo-navarro-restrict-default-to-relative-paths
Move protocol-relative RelayState guard before URI parsing
2 parents 10bfc7a + 8bfba33 commit 8eec97b

File tree

3 files changed

+55
-61
lines changed

3 files changed

+55
-61
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ Note that when [integrating with Devise](#devise-integration), the URL path will
107107

108108
* `:slo_relay_state_validator` - A callable used to validate any RelayState before OmniAuth uses it for
109109
redirects in Single Logout flows. The callable receives the RelayState value and, if it accepts a
110-
second argument, the current Rack request. The default validator allows relative paths beginning
111-
with `/` and absolute `http` or `https` URLs, and rejects invalid URIs, protocol-relative URLs, and
112-
other schemes. Defaults generated via `:slo_default_relay_state` are assumed to be safe and skipped by
113-
this validation step. Optional. When set to `true`, every RelayState value is accepted. When set to
110+
second argument, the current Rack request. The default validator allows only relative paths beginning
111+
with `/` and rejects absolute URLs, invalid URIs, protocol-relative URLs, and other schemes. Defaults
112+
generated via `:slo_default_relay_state` are assumed to be safe and skipped by this validation step.
113+
Optional. When set to `true`, every RelayState value is accepted. When set to
114114
`false` (or any other falsy value), every provided RelayState is rejected and the strategy falls back
115115
to the default RelayState. See the SLO relay state validator specs in
116116
[`spec/omniauth/strategies/saml_spec.rb`](spec/omniauth/strategies/saml_spec.rb) for additional

lib/omniauth/strategies/saml.rb

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,18 @@ def self.inherited(subclass)
3131
DEFAULT_SLO_RELAY_STATE_VALIDATOR = lambda do |relay_state, _request|
3232
return true if relay_state.nil? || relay_state == ""
3333

34+
return false if relay_state.start_with?("//")
35+
3436
begin
3537
uri = URI.parse(relay_state)
3638
rescue URI::Error
3739
return false
3840
end
3941

40-
if uri.scheme.nil?
41-
return false if relay_state.start_with?("//")
42+
return false unless uri.relative?
4243

43-
path = uri.path
44-
path && path.start_with?("/")
45-
else
46-
%w[http https].include?(uri.scheme) && !uri.host.nil?
47-
end
44+
path = uri.path
45+
path && path.start_with?("/")
4846
end
4947

5048
option :slo_default_relay_state

spec/omniauth/strategies/saml_spec.rb

Lines changed: 46 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -276,70 +276,64 @@ def post_xml(xml = :example_response, opts = {})
276276
end
277277

278278
context "when response is a logout response" do
279-
before :each do
280-
post "/auth/saml/slo", {
279+
let(:opts) do
280+
{ "rack.session" => { "saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9" } }
281+
end
282+
283+
let(:params) do
284+
{
281285
SAMLResponse: load_xml(:example_logout_response),
282-
RelayState: "https://example.com/",
283-
}, "rack.session" => {"saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9"}
286+
RelayState: relay_state,
287+
}
284288
end
285289

286-
it "should redirect to relaystate" do
287-
expect(last_response).to be_redirect
288-
expect(last_response.location).to match /https:\/\/example.com\//
290+
subject(:post_slo_response) { post "/auth/saml/slo", params, opts }
291+
292+
context "when relay state is relative" do
293+
let(:relay_state) { "/signed-out" }
294+
295+
it "redirects to the relaystate" do
296+
post_slo_response
297+
298+
expect(last_response).to be_redirect
299+
expect(last_response.location).to eq "/signed-out"
300+
end
301+
end
302+
303+
context "when relay state is an absolute https URL" do
304+
let(:relay_state) { "https://example.com/" }
305+
306+
it "raises an invalid relay state error" do
307+
expect { post_slo_response }.
308+
to raise_error(OmniAuth::Strategies::SAML::ValidationError, "Invalid RelayState")
309+
end
289310
end
290311

291312
context 'when slo_default_relay_state is present' do
292313
let(:saml_options) { super().merge(slo_default_relay_state: '/signed-out') }
293314

294315
context "when response relay state is invalid" do
295-
let(:params) do
296-
{
297-
SAMLResponse: load_xml(:example_logout_response),
298-
RelayState: "https://example.com/",
299-
}
300-
end
301-
302-
let(:opts) do
303-
{ "rack.session" => { "saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9" } }
304-
end
305-
306-
subject(:post_slo_response) { post "/auth/saml/slo", params, opts }
316+
let(:relay_state) { "https://example.com/" }
307317

308318
[
309319
"//attacker.test",
310320
"javascript:alert(1)",
321+
"https://example.com/logout",
311322
].each do |unsafe_relay_state|
312323
context "#{unsafe_relay_state}" do
313-
let(:params) { super().merge(RelayState: unsafe_relay_state) }
324+
let(:relay_state) { unsafe_relay_state }
314325

315326
it { is_expected.to be_redirect.and have_attributes(location: "/signed-out") }
316327
end
317328
end
318-
319-
context 'when absolute https relay state' do
320-
let(:params) { super().merge(RelayState: "https://example.com/logout") }
321-
322-
it { is_expected.to be_redirect.and have_attributes(location: "https://example.com/logout") }
323-
end
324329
end
325330
end
326331

327332
context 'when slo_default_relay_state is blank' do
328333
let(:saml_options) { super().merge(slo_default_relay_state: nil) }
329334

330335
context "when response relay state is invalid" do
331-
let(:params) do
332-
{
333-
SAMLResponse: load_xml(:example_logout_response),
334-
RelayState: "javascript:alert(1)",
335-
}
336-
end
337-
338-
let(:opts) do
339-
{ "rack.session" => { "saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9" } }
340-
end
341-
342-
subject(:post_slo_response) { post "/auth/saml/slo", params, opts }
336+
let(:relay_state) { "javascript:alert(1)" }
343337

344338
it { expect { post_slo_response }.to raise_error(OmniAuth::Strategies::SAML::ValidationError, "Invalid RelayState") }
345339
end
@@ -349,27 +343,33 @@ def post_xml(xml = :example_response, opts = {})
349343
context "when request is a logout request" do
350344
subject { post "/auth/saml/slo", params, "rack.session" => { "saml_uid" => "[email protected]" } }
351345

346+
let(:relay_state) { "https://example.com/" }
347+
352348
let(:params) do
353349
{
354350
"SAMLRequest" => load_xml(:example_logout_request),
355-
"RelayState" => "https://example.com/",
351+
"RelayState" => relay_state,
356352
}
357353
end
358354

359355
context "when logout request is valid" do
356+
let(:relay_state) { "/logout" }
357+
360358
before { subject }
361359

362360
it "should redirect to logout response" do
363361
expect(last_response).to be_redirect
364362
expect(last_response.location).to match /https:\/\/idp.sso.example.com\/signoff\/29490/
365-
expect(last_response.location).to match /RelayState=https%3A%2F%2Fexample.com%2F/
363+
expect(last_response.location).to match /RelayState=%2Flogout/
366364
end
367365
end
368366

369367
context 'when slo_default_relay_state is present' do
370368
let(:saml_options) { super().merge(slo_default_relay_state: '/signed-out') }
371369

372370
context "when request relay state is invalid" do
371+
let(:relay_state) { "https://example.com/" }
372+
373373
let(:params) do
374374
{
375375
"SAMLRequest" => load_xml(:example_logout_request),
@@ -380,6 +380,7 @@ def post_xml(xml = :example_response, opts = {})
380380
[
381381
"//attacker.test",
382382
"javascript:alert(1)",
383+
"https://example.com/logout",
383384
].each do |unsafe_relay_state|
384385
context "when the relay state is #{unsafe_relay_state}" do
385386
let(:relay_state) { unsafe_relay_state }
@@ -397,11 +398,6 @@ def post_xml(xml = :example_response, opts = {})
397398
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
398399
end
399400

400-
context "when the relay state is an https URL" do
401-
let(:relay_state) { "https://example.com/logout" }
402-
403-
it { is_expected.to have_attributes(location: a_string_matching(/RelayState=https%3A%2F%2Fexample.com%2Flogout/)) }
404-
end
405401
end
406402

407403
context "with validators that resolve to falsy" do
@@ -540,15 +536,15 @@ def test_default_relay_state(static_default_relay_state = nil, &block_default_re
540536
end
541537
end
542538

543-
context 'when slo_default_relay_state is present' do
544-
let(:saml_options) { super().merge(slo_default_relay_state: '/signed-out') }
545-
let(:params) { {} }
546-
subject { post "/auth/saml/spslo", params }
539+
context 'when slo_default_relay_state is present' do
540+
let(:saml_options) { super().merge(slo_default_relay_state: '/signed-out') }
541+
let(:params) { {} }
542+
subject { post "/auth/saml/spslo", params }
547543

548544
context 'with an https relay state' do
549545
let(:params) { { RelayState: "https://example.com/logout" } }
550546

551-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=https%3A%2F%2Fexample.com%2Flogout/)) }
547+
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
552548
end
553549

554550
context 'with a protocol relative relay state' do

0 commit comments

Comments
 (0)