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
@@ -280,17 +329,12 @@ def post_xml(xml = :example_response, opts = {})
280329 { "rack.session" => { "saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9" } }
281330 end
282331
283- let ( :params ) do
284- {
285- SAMLResponse : load_xml ( :example_logout_response ) ,
286- RelayState : relay_state ,
287- }
288- end
332+ let ( :params ) { { SAMLResponse : load_xml ( :example_logout_response ) } }
289333
290334 subject ( :post_slo_response ) { post "/auth/saml/slo" , params , opts }
291335
292336 context "when relay state is relative" do
293- let ( :relay_state ) { "/signed-out" }
337+ let ( :params ) { super ( ) . merge ( RelayState : "/signed-out" ) }
294338
295339 it "redirects to the relaystate" do
296340 post_slo_response
@@ -301,7 +345,7 @@ def post_xml(xml = :example_response, opts = {})
301345 end
302346
303347 context "when relay state is an absolute https URL" do
304- let ( :relay_state ) { "https://example.com/" }
348+ let ( :params ) { super ( ) . merge ( RelayState : "https://example.com/" ) }
305349
306350 it "redirects without a location header" do
307351 post_slo_response
@@ -314,35 +358,32 @@ def post_xml(xml = :example_response, opts = {})
314358 context 'when slo_default_relay_state is present' do
315359 let ( :saml_options ) { super ( ) . merge ( slo_default_relay_state : '/signed-out' ) }
316360
361+ context "when response relay state is valid" do
362+ let ( :params ) { super ( ) . merge ( RelayState : "/safe/logout" ) }
363+
364+ it { is_expected . to be_redirect . and have_attributes ( location : '/safe/logout' ) }
365+ end
366+
317367 context "when response relay state is invalid" do
318- let ( :relay_state ) { "https://example.com/" }
319-
320- [
321- "//attacker.test" ,
322- "javascript:alert(1)" ,
323- "https://example.com/logout" ,
324- ] . each do |unsafe_relay_state |
325- context "#{ unsafe_relay_state } " do
326- let ( :relay_state ) { unsafe_relay_state }
327-
328- it { is_expected . to be_redirect . and have_attributes ( location : "/signed-out" ) }
329- end
330- end
368+ let ( :params ) { super ( ) . merge ( RelayState : "javascript:alert(1)" ) }
369+
370+ it { is_expected . to be_redirect . and have_attributes ( location : '/signed-out' ) }
331371 end
332372 end
333373
334374 context 'when slo_default_relay_state is blank' do
335375 let ( :saml_options ) { super ( ) . merge ( slo_default_relay_state : nil ) }
336376
337- context "when response relay state is invalid " do
338- let ( :relay_state ) { "javascript:alert(1)" }
377+ context "when response relay state is valid " do
378+ let ( :params ) { super ( ) . merge ( RelayState : "/safe/logout" ) }
339379
340- it "redirects without a location header" do
341- post_slo_response
380+ it { is_expected . to be_redirect . and have_attributes ( location : '/safe/logout' ) }
381+ end
342382
343- expect ( last_response ) . to be_redirect
344- expect ( last_response . headers . fetch ( "Location" ) ) . to be_nil
345- end
383+ context "when response relay state is invalid" do
384+ let ( :params ) { super ( ) . merge ( RelayState : "javascript:alert(1)" ) }
385+
386+ it { is_expected . to be_redirect . and have_attributes ( location : nil ) }
346387 end
347388 end
348389 end
@@ -374,78 +415,7 @@ def post_xml(xml = :example_response, opts = {})
374415 context 'when slo_default_relay_state is present' do
375416 let ( :saml_options ) { super ( ) . merge ( slo_default_relay_state : '/signed-out' ) }
376417
377- context "when request relay state is invalid" do
378- let ( :relay_state ) { "https://example.com/" }
379-
380- let ( :params ) do
381- {
382- "SAMLRequest" => load_xml ( :example_logout_request ) ,
383- "RelayState" => relay_state ,
384- }
385- end
386-
387- [
388- "//attacker.test" ,
389- "javascript:alert(1)" ,
390- "https://example.com/logout" ,
391- ] . each do |unsafe_relay_state |
392- context "when the relay state is #{ unsafe_relay_state } " do
393- let ( :relay_state ) { unsafe_relay_state }
394-
395- it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
396- end
397- end
398-
399- context "when the validator rejects the default relay state" do
400- let ( :relay_state ) { "javascript:alert(1)" }
401- let ( :saml_options ) do
402- super ( ) . merge ( slo_relay_state_validator : proc { |value | value . start_with? ( "https://" ) } )
403- end
404-
405- it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
406- end
407-
408- end
409-
410- context "with validators that resolve to falsy" do
411- let ( :params ) { super ( ) . merge ( "RelayState" => "javascript:alert(1)" ) }
412-
413- context "when the validator option is nil" do
414- let ( :saml_options ) { super ( ) . merge ( slo_relay_state_validator : nil ) }
415-
416- it { is_expected . to have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
417- end
418-
419- context "when the validator option is false" do
420- let ( :saml_options ) { super ( ) . merge ( slo_relay_state_validator : false ) }
421-
422- it { is_expected . to have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
423- end
424-
425- context "when the validator option is true" do
426- let ( :saml_options ) { super ( ) . merge ( slo_relay_state_validator : true ) }
427-
428- it { is_expected . to have_attributes ( location : a_string_matching ( /RelayState=javascript%3Aalert%281%29/ ) ) }
429- end
430-
431- context "when the validator returns false" do
432- let ( :saml_options ) do
433- super ( ) . merge ( slo_relay_state_validator : proc { |relay_state | relay_state == "/signed-out" } )
434- end
435-
436- it { is_expected . to have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
437- end
438-
439- context "when the validator returns nil" do
440- let ( :saml_options ) do
441- super ( ) . merge (
442- slo_relay_state_validator : proc { |relay_state | relay_state == "/signed-out" ? true : nil } ,
443- )
444- end
445-
446- it { is_expected . to have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
447- end
448- end
418+ it_behaves_like 'validating RelayState param'
449419 end
450420
451421 context 'when slo_default_relay_state is blank' do
@@ -546,72 +516,12 @@ def test_default_relay_state(static_default_relay_state = nil, &block_default_re
546516 end
547517 end
548518
549- context 'when slo_default_relay_state is present' do
550- let ( :saml_options ) { super ( ) . merge ( slo_default_relay_state : '/signed-out' ) }
551- let ( :params ) { { } }
552- subject { post "/auth/saml/spslo" , params }
553-
554- context 'with an https relay state' do
555- let ( :params ) { { RelayState : "https://example.com/logout" } }
556-
557- it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
558- end
559-
560- context 'with a protocol relative relay state' do
561- let ( :params ) { { RelayState : "//attacker.test" } }
562-
563- it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
564- end
565-
566- context 'with a javascript relay state' do
567- let ( :params ) { { RelayState : "javascript:alert(1)" } }
568-
569- it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
570-
571- context 'when the validator would reject the default' do
572- let ( :saml_options ) do
573- super ( ) . merge ( slo_relay_state_validator : proc { |value | value . start_with? ( "https://" ) } )
574- end
575-
576- it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
577- end
578-
579- context 'when the validator is nil' do
580- let ( :saml_options ) { super ( ) . merge ( slo_relay_state_validator : nil ) }
581-
582- it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
583- end
584-
585- context 'when the validator is false' do
586- let ( :saml_options ) { super ( ) . merge ( slo_relay_state_validator : false ) }
587-
588- it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
589- end
590-
591- context 'when the validator is true' do
592- let ( :saml_options ) { super ( ) . merge ( slo_relay_state_validator : true ) }
593-
594- it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=javascript%3Aalert%281%29/ ) ) }
595- end
596-
597- context 'when the validator returns false' do
598- let ( :saml_options ) do
599- super ( ) . merge ( slo_relay_state_validator : proc { |state | state == "/signed-out" } )
600- end
601-
602- it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
603- end
604-
605- context 'when the validator returns nil' do
606- let ( :saml_options ) do
607- super ( ) . merge (
608- slo_relay_state_validator : proc { |state | state == "/signed-out" ? true : nil } ,
609- )
610- end
519+ context 'when slo_default_relay_state is present' do
520+ let ( :saml_options ) { super ( ) . merge ( slo_default_relay_state : '/signed-out' ) }
521+ let ( :params ) { { } }
522+ subject { post "/auth/saml/spslo" , params }
611523
612- it { is_expected . to be_redirect . and have_attributes ( location : a_string_matching ( /RelayState=%2Fsigned-out/ ) ) }
613- end
614- end
524+ it_behaves_like 'validating RelayState param'
615525
616526 context 'when using a custom default relay state' do
617527 let ( :saml_options ) do
0 commit comments