diff --git a/Gemfile b/Gemfile index 9f336726..8ed611c9 100644 --- a/Gemfile +++ b/Gemfile @@ -8,5 +8,5 @@ group :ldap do end group :active_resource do - gem "activeresource", ">= 2.3.12", "< 4.0" + gem "activeresource", ">= 2.3.12", "< 5.2" end diff --git a/config/config.example.yml b/config/config.example.yml index f49c5211..772ec4ec 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -534,10 +534,14 @@ log: #downcase_username: true # If you'd like to limit the service hosts that can use CAS for authentication, -# add the individual IPs and IP ranges in CIDR notation below. Leaving this -# setting blank will allow any server to authenticate users via the CAS server +# you can either limit specific service URIs (validating the service parameter), +# or add the individual IPs and IP ranges in CIDR notation below. Leaving these +# settings blank will allow any server to authenticate users via the CAS server # and potentially harvest sensitive user information. +#allowed_service_uris: +# - https://myservice.com/login/cas + #allowed_service_ips: # - 127.0.0.1 # - 192.168.0.0/24 diff --git a/db/migrate/001_create_initial_structure.rb b/db/migrate/001_create_initial_structure.rb index e381acf3..efffc022 100644 --- a/db/migrate/001_create_initial_structure.rb +++ b/db/migrate/001_create_initial_structure.rb @@ -1,4 +1,4 @@ -class CreateInitialStructure < ActiveRecord::Migration +class CreateInitialStructure < ActiveRecord::Migration[4.2] def self.up # Oracle table names cannot exceed 30 chars... # See http://code.google.com/p/rubycas-server/issues/detail?id=15 diff --git a/db/migrate/002_add_indexes_for_performance.rb b/db/migrate/002_add_indexes_for_performance.rb index 680ecf77..454aface 100644 --- a/db/migrate/002_add_indexes_for_performance.rb +++ b/db/migrate/002_add_indexes_for_performance.rb @@ -1,4 +1,4 @@ -class AddIndexesForPerformance < ActiveRecord::Migration +class AddIndexesForPerformance < ActiveRecord::Migration[4.2] def self.up add_index :casserver_lt, :ticket add_index :casserver_st, :ticket diff --git a/lib/casserver/model/consumable.rb b/lib/casserver/model/consumable.rb index eeb6b62e..6b224b44 100644 --- a/lib/casserver/model/consumable.rb +++ b/lib/casserver/model/consumable.rb @@ -18,12 +18,12 @@ def cleanup(max_lifetime, max_unconsumed_lifetime) Time.now - max_lifetime, Time.now - max_unconsumed_lifetime] - expired_tickets_count = count(:conditions => conditions) + expired_tickets_count = where(conditions).count $LOG.debug("Destroying #{expired_tickets_count} expired #{self.name.demodulize}"+ "#{'s' if expired_tickets_count > 1}.") if expired_tickets_count > 0 - destroy_all(conditions) + where(conditions).destroy_all end end end diff --git a/lib/casserver/server.rb b/lib/casserver/server.rb index 36c9c363..c03f0d98 100644 --- a/lib/casserver/server.rb +++ b/lib/casserver/server.rb @@ -322,7 +322,11 @@ def self.init_database! begin if @service - if @renew + if !service_allowed?(@service) + $LOG.warn("Disallowed service '#{@service}'.") + @message = {:type => 'mistake', + :message => t.error.disallowed_service} + elsif @renew $LOG.info("Authentication renew explicitly requested. Proceeding with CAS login for service #{@service.inspect}.") elsif tgt && !tgt_error $LOG.debug("Valid ticket granting ticket detected.") @@ -409,6 +413,13 @@ def self.init_database! @username.downcase! end + if !@service.blank? && !service_allowed?(@service) + @message = {:type => 'mistake', + :message => t.error.disallowed_service} + status 400 + return render @template_engine, :login + end + if error = validate_login_ticket(@lt) @message = {:type => 'mistake', :message => error} # generate another login ticket to allow for re-submitting the form @@ -532,9 +543,8 @@ def self.init_database! st.destroy end - pgts = CASServer::Model::ProxyGrantingTicket.find(:all, - :conditions => [CASServer::Model::ServiceTicket.quoted_table_name+".username = ?", tgt.username], - :include => :service_ticket) + pgts = CASServer::Model::ProxyGrantingTicket.where(casserver_st: { username: tgt.username }). + joins(:service_ticket) pgts.each do |pgt| $LOG.debug("Deleting Proxy-Granting Ticket '#{pgt}' for user '#{pgt.service_ticket.username}'") pgt.destroy @@ -766,6 +776,12 @@ def compile_template(engine, data, options, views) super engine, data, options, views end + def service_allowed?(service) + allowed_uris = Array(settings.config[:allowed_service_uris]) + + allowed_uris.empty? || allowed_uris.include?(service) + end + def ip_allowed?(ip) require 'ipaddr' diff --git a/locales/en.yml b/locales/en.yml index 99eb6094..889d07f1 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -9,6 +9,7 @@ error: invalid_target_service: "The target service your browser supplied appears to be invalid. Please contact your system administrator for help." unable_to_authenticate: "The client and server are unable to negotiate authentication. Please try logging in again later." no_service_parameter_given: "The server cannot fulfill this gateway request because no service parameter was given." + disallowed_service: "The target service is not allowed to authenticate against this CAS server. Please contact your system administrator for help." notice: logged_in_as: "You are currently logged in as '%1'. If this is not you, please log in below." diff --git a/rubycas-server.gemspec b/rubycas-server.gemspec index 2076d106..3d7486a5 100644 --- a/rubycas-server.gemspec +++ b/rubycas-server.gemspec @@ -26,17 +26,17 @@ $gemspec = Gem::Specification.new do |s| s.has_rdoc = true s.post_install_message = "For more information on RubyCAS-Server, see http://rubycas.github.com" - s.add_dependency("activerecord", ">= 2.3.12", "< 4.0") - s.add_dependency("activesupport", ">= 2.3.12", "< 4.0") + s.add_dependency("activerecord", ">= 3.0", "< 5.2") + s.add_dependency("activesupport", ">= 3.0", "< 5.2") s.add_dependency("sinatra", "~> 1.0") - s.add_dependency("sinatra-r18n", '~> 1.1.0') + s.add_dependency("sinatra-r18n", '~> 2.1.7') s.add_dependency("crypt-isaac", "~> 0.9.1") s.add_development_dependency("rack-test") s.add_development_dependency("capybara", '1.1.2') s.add_development_dependency("rspec") s.add_development_dependency("rspec-core") - s.add_development_dependency("rake", "0.8.7") + s.add_development_dependency("rake", "~> 11.0") s.add_development_dependency("sqlite3", "~> 1.3.1") s.add_development_dependency("appraisal", "~> 0.4.1") s.add_development_dependency("guard", "~> 1.4.0") diff --git a/spec/casserver/utils_spec.rb b/spec/casserver/utils_spec.rb index 33525379..8cafdf5e 100644 --- a/spec/casserver/utils_spec.rb +++ b/spec/casserver/utils_spec.rb @@ -11,13 +11,15 @@ module CASServer let(:params_with_password_filtered) { { 'password' => '******' } } it 'should log the controller action' do - $LOG.should_receive(:debug).with 'Processing application::instance_eval {}' + $LOG.should_receive(:<<).with "\n" + $LOG.should_receive(:debug).with 'Processing application::instance_exec {}' subject.log_controller_action('application', params) end it 'should filter password parameters in the log' do - $LOG.should_receive(:debug).with "Processing application::instance_eval #{params_with_password_filtered.inspect}" + $LOG.should_receive(:<<).with "\n" + $LOG.should_receive(:debug).with "Processing application::instance_exec #{params_with_password_filtered.inspect}" subject.log_controller_action('application', params_with_password) end diff --git a/spec/casserver_spec.rb b/spec/casserver_spec.rb index b49c1a03..ba81a740 100644 --- a/spec/casserver_spec.rb +++ b/spec/casserver_spec.rb @@ -36,6 +36,18 @@ page.should have_content("You have successfully logged in") end + it "auto-forwards when you're already logged in" do + visit "/login" + + fill_in 'username', :with => VALID_USERNAME + fill_in 'password', :with => VALID_PASSWORD + click_button 'login-submit' + + visit "/login?service="+CGI.escape(@target_service) + + page.current_url.should =~ /^#{Regexp.escape(@target_service)}\/?\?ticket=ST\-[1-9rA-Z]+/ + end + it "fails to log in with invalid password" do visit "/login" fill_in 'username', :with => VALID_USERNAME @@ -92,6 +104,38 @@ #page.should have_xpath("<script>alert(32)</script>") end + describe 'service_uri validation' do + let(:service) { 'http://imposter.com/' } + + it "doesn't redirect back to untrusted services" do + visit "/login?service="+CGI.escape(service) + + page.should have_content("The target service is not allowed") + + fill_in 'username', :with => VALID_USERNAME + fill_in 'password', :with => VALID_PASSWORD + + click_button 'login-submit' + + page.should have_content("The target service is not allowed") + end + + it "doesn't redirect back when already logged in" do + visit "/login" + + fill_in 'username', :with => VALID_USERNAME + fill_in 'password', :with => VALID_PASSWORD + + click_button 'login-submit' + + visit "/login?service="+CGI.escape(service) + + page.should have_content("The target service is not allowed") + end + + end + + end # describe '/login' diff --git a/spec/config/default_config.yml b/spec/config/default_config.yml index 90a83914..aa7180ca 100644 --- a/spec/config/default_config.yml +++ b/spec/config/default_config.yml @@ -50,4 +50,7 @@ enable_single_sign_out: true #downcase_username: true allowed_service_ips: - - 127.0.0.0/24 \ No newline at end of file + - 127.0.0.0/24 + +allowed_service_uris: + - http://my.app.test