3030 end
3131 let ( :strategy ) { [ OmniAuth ::Strategies ::SAML , saml_options ] }
3232
33+ shared_examples 'validating RelayState param' do
34+ context 'when slo_relay_state_validator is not defined and default' do
35+ [
36+ [ '/signed-out' , '//attacker.test' , '%2Fsigned-out' ] ,
37+ [ '/signed-out' , 'javascript:alert(1)' , '%2Fsigned-out' ] ,
38+ [ '/signed-out' , 'https://example.com/logout' , '%2Fsigned-out' ] ,
39+ [ '/signed-out' , 'https://example.com/logout?param=1&two=two' , '%2Fsigned-out' ] ,
40+ [ '/signed-out' , '/' , '%2F' ] ,
41+ [ '' , '//attacker.test' , '' ] ,
42+ [ '' , '/team/logout' , '%2Fteam%2Flogout' ] ,
43+ ] . each do |slo_default_relay_state , relay_state_param , expected_relay_state |
44+ context "when slo_default_relay_state: #{ slo_default_relay_state . inspect } , relay_state_param: #{ relay_state_param . inspect } " do
45+ let ( :saml_options ) { super ( ) . merge ( slo_default_relay_state : slo_default_relay_state ) }
46+ let ( :params ) { super ( ) . merge ( 'RelayState' => relay_state_param ) }
47+
48+ it { is_expected . to be_redirect . and have_attributes ( location : a_string_including ( "RelayState=#{ expected_relay_state } " ) ) }
49+ end
50+ end
51+ end
52+
53+ context 'when slo_relay_state_validator is overridden' do
54+ [
55+ [ '/signed-out' , proc { |state | state . start_with? ( 'https://trusted.example.com' ) } , 'https://trusted.example.com/logout' , 'https%3A%2F%2Ftrusted.example.com%2Flogout' ] ,
56+ [ '/signed-out' , proc { |state | state . start_with? ( 'https://trusted.example.com' ) } , 'https://attacker.test/logout' , '%2Fsigned-out' ] ,
57+ [ '/signed-out' , proc { |state | state . start_with? ( 'https://trusted.example.com' ) } , '/safe/path' , '%2Fsigned-out' ] ,
58+ [ '/signed-out' , proc { |state , req | state == req . params [ 'RelayState' ] } , '/team/logout' , '%2Fteam%2Flogout' ] ,
59+ [ '/signed-out' , nil , '//attacker.test' , '%2Fsigned-out' ] ,
60+ [ '/signed-out' , false , '//attacker.test' , '%2Fsigned-out' ] ,
61+ [ '/signed-out' , proc { |_ | false } , '//attacker.test' , '%2Fsigned-out' ] ,
62+ [ '/signed-out' , proc { |_ | true } , 'javascript:alert(1)' , 'javascript%3Aalert%281%29' ] ,
63+ [ nil , true , 'https://example.com/logout' , 'https%3A%2F%2Fexample.com%2Flogout' ] ,
64+ [ nil , true , 'javascript:alert(1)' , 'javascript%3Aalert%281%29' ] ,
65+ [ nil , true , '/' , '%2F' ] ,
66+ ] . each do |slo_default_relay_state , slo_relay_state_validator , relay_state_param , expected_relay_state |
67+ context "when slo_default_relay_state: #{ slo_default_relay_state . inspect } , slo_relay_state_validator: #{ slo_relay_state_validator . inspect } , relay_state_param: #{ relay_state_param . inspect } " do
68+ let ( :saml_options ) do
69+ super ( ) . merge (
70+ slo_default_relay_state : slo_default_relay_state ,
71+ slo_relay_state_validator : slo_relay_state_validator ,
72+ )
73+ end
74+ let ( :params ) { super ( ) . merge ( 'RelayState' => relay_state_param ) }
75+
76+ it { is_expected . to be_redirect . and have_attributes ( location : a_string_including ( "RelayState=#{ expected_relay_state } " ) ) }
77+ end
78+ end
79+ end
80+ end
81+
3382 describe 'POST /auth/saml' do
3483 context 'without idp runtime params present' do
3584 before do
274323 end
275324
276325 context "when response is a logout response" do
277- before :each do
278- post "/auth/saml/slo" , {
279- SAMLResponse : load_xml ( :example_logout_response ) ,
280- RelayState : "https://example.com/" ,
281- } , "rack.session" => { "saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9" }
326+ let ( :opts ) do
327+ { "rack.session" => { "saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9" } }
282328 end
283329
284- it "should redirect to relaystate" do
285- expect ( last_response ) . to be_redirect
286- expect ( last_response . location ) . to match /https:\/ \/ example.com\/ /
330+ let ( :params ) { { SAMLResponse : load_xml ( :example_logout_response ) } }
331+
332+ subject ( :post_slo_response ) { post "/auth/saml/slo" , params , opts }
333+
334+ context "when relay state is relative" do
335+ let ( :params ) { super ( ) . merge ( RelayState : "/signed-out" ) }
336+
337+ it "redirects to the relaystate" do
338+ post_slo_response
339+
340+ expect ( last_response ) . to be_redirect
341+ expect ( last_response . location ) . to eq "/signed-out"
342+ end
343+ end
344+
345+ context "when relay state is an absolute https URL" do
346+ let ( :params ) { super ( ) . merge ( RelayState : "https://example.com/" ) }
347+
348+ it "redirects without a location header" do
349+ post_slo_response
350+
351+ expect ( last_response ) . to be_redirect
352+ expect ( last_response . headers . fetch ( "Location" ) ) . to be_nil
353+ end
354+ end
355+
356+ context 'when slo_default_relay_state is present' do
357+ let ( :saml_options ) { super ( ) . merge ( slo_default_relay_state : '/signed-out' ) }
358+
359+ context "when response relay state is valid" do
360+ let ( :params ) { super ( ) . merge ( RelayState : "/safe/logout" ) }
361+
362+ it { is_expected . to be_redirect . and have_attributes ( location : '/safe/logout' ) }
363+ end
364+
365+ context "when response relay state is invalid" do
366+ let ( :params ) { super ( ) . merge ( RelayState : "javascript:alert(1)" ) }
367+
368+ it { is_expected . to be_redirect . and have_attributes ( location : '/signed-out' ) }
369+ end
370+ end
371+
372+ context 'when slo_default_relay_state is blank' do
373+ let ( :saml_options ) { super ( ) . merge ( slo_default_relay_state : nil ) }
374+
375+ context "when response relay state is valid" do
376+ let ( :params ) { super ( ) . merge ( RelayState : "/safe/logout" ) }
377+
378+ it { is_expected . to be_redirect . and have_attributes ( location : '/safe/logout' ) }
379+ end
380+
381+ context "when response relay state is invalid" do
382+ let ( :params ) { super ( ) . merge ( RelayState : "javascript:alert(1)" ) }
383+
384+ it { is_expected . to be_redirect . and have_attributes ( location : nil ) }
385+ end
287386 end
288387 end
289388
290389 context "when request is a logout request" do
291390 subject { post "/auth/saml/slo" , params , "rack.session" => { "saml_uid" => "[email protected] " } } 292391
392+ let ( :relay_state ) { "https://example.com/" }
393+
293394 let ( :params ) do
294395 {
295396 "SAMLRequest" => load_xml ( :example_logout_request ) ,
296- "RelayState" => "https://example.com/" ,
397+ "RelayState" => relay_state ,
297398 }
298399 end
299400
300401 context "when logout request is valid" do
402+ let ( :relay_state ) { "/logout" }
403+
301404 before { subject }
302405
303406 it "should redirect to logout response" do
304407 expect ( last_response ) . to be_redirect
305408 expect ( last_response . location ) . to match /https:\/ \/ idp.sso.example.com\/ signoff\/ 29490/
306- expect ( last_response . location ) . to match /RelayState=https%3A%2F%2Fexample.com%2F/
409+ expect ( last_response . location ) . to match /RelayState=%2Flogout/
410+ end
411+ end
412+
413+ it_behaves_like 'validating RelayState param'
414+
415+ context 'when slo_default_relay_state is blank' do
416+ let ( :saml_options ) { super ( ) . merge ( slo_default_relay_state : nil ) }
417+
418+ context "when request relay state is invalid" do
419+ let ( :params ) do
420+ {
421+ "SAMLRequest" => load_xml ( :example_logout_request ) ,
422+ "RelayState" => "javascript:alert(1)" ,
423+ }
424+ end
425+
426+ it "redirects without including a RelayState parameter" do
427+ subject
428+
429+ expect ( last_response ) . to be_redirect
430+ expect ( last_response . location ) . to match %r{https://idp\. sso\. example\. com/signoff/29490}
431+ expect ( last_response . location ) . not_to match ( /RelayState=/ )
432+ end
433+ end
434+ end
435+
436+ context "with a custom relay state validator" do
437+ let ( :saml_options ) do
438+ super ( ) . merge (
439+ slo_relay_state_validator : proc do |relay_state , rack_request |
440+ expect ( rack_request ) . to respond_to ( :params )
441+ relay_state == "custom-state"
442+ end ,
443+ )
307444 end
445+ let ( :params ) { super ( ) . merge ( "RelayState" => "custom-state" ) }
446+
447+ it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=custom-state/ ) ) }
308448 end
309449
310450 context "when request is an invalid logout request" do
345485 end
346486
347487 describe 'POST /auth/saml/spslo' do
488+ let ( :params ) { { } }
489+ subject { post "/auth/saml/spslo" , params }
490+
348491 def test_default_relay_state ( static_default_relay_state = nil , &block_default_relay_state )
349492 saml_options [ "slo_default_relay_state" ] = static_default_relay_state || block_default_relay_state
350493 post "/auth/saml/spslo"
@@ -370,6 +513,21 @@ def test_default_relay_state(static_default_relay_state = nil, &block_default_re
370513 end
371514 end
372515
516+ it_behaves_like 'validating RelayState param'
517+
518+ context 'when slo_default_relay_state is blank' do
519+ let ( :saml_options ) { super ( ) . merge ( slo_default_relay_state : nil ) }
520+ let ( :params ) { { RelayState : "//example.com" } }
521+
522+ it "redirects without including a RelayState parameter" do
523+ subject
524+
525+ expect ( last_response ) . to be_redirect
526+ expect ( last_response . location ) . to match %r{https://idp\. sso\. example\. com/signoff/29490}
527+ expect ( last_response . location ) . not_to match ( /RelayState=/ )
528+ end
529+ end
530+
373531 it "should give not implemented without an idp_slo_service_url" do
374532 saml_options . delete ( :idp_slo_service_url )
375533 post "/auth/saml/spslo"
0 commit comments