From 1be41b92d9c71a5e6cf9c41abba8a6658c2b7935 Mon Sep 17 00:00:00 2001 From: Adam Crownoble Date: Wed, 12 Dec 2012 22:09:24 -0800 Subject: [PATCH] Add support for allowed_service_ips whitelist. allowed_service_ips can be set in config.yml to limit service validations to a certain set of IPs or IP ranges. This prevents just any site from being able to grab potentially sensitive personal information. --- config/config.example.yml | 9 +++ lib/casserver/server.rb | 142 +++++++++++++++++++-------------- spec/casserver_spec.rb | 79 +++++++++++++++--- spec/config/default_config.yml | 3 + spec/spec_helper.rb | 9 +++ 5 files changed, 170 insertions(+), 72 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 6a523097..ea3c621e 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -538,3 +538,12 @@ log: # convert this to "jsmith". #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 +# and potentially harvest sensitive user information. + +#allowed_service_ips: +# - 127.0.0.1 +# - 192.168.0.0/24 \ No newline at end of file diff --git a/lib/casserver/server.rb b/lib/casserver/server.rb index 70a90e38..5918598d 100644 --- a/lib/casserver/server.rb +++ b/lib/casserver/server.rb @@ -601,60 +601,70 @@ def self.init_database! # 2.4 # 2.4.1 - get "#{uri_path}/validate" do - CASServer::Utils::log_controller_action(self.class, params) - - # required - @service = clean_service_url(params['service']) - @ticket = params['ticket'] - # optional - @renew = params['renew'] - - st, @error = validate_service_ticket(@service, @ticket) - @success = st && !@error - - @username = st.username if @success - + get "#{uri_path}/validate" do + CASServer::Utils::log_controller_action(self.class, params) + + if ip_allowed?(request.ip) + # required + @service = clean_service_url(params['service']) + @ticket = params['ticket'] + # optional + @renew = params['renew'] + + st, @error = validate_service_ticket(@service, @ticket) + @success = st && !@error + + @username = st.username if @success + else + @success = false + @error = Error.new(:INVALID_REQUEST, 'The IP address of this service has not been allowed') + end + status response_status_from_error(@error) if @error - - render @template_engine, :validate, :layout => false - end + + render @template_engine, :validate, :layout => false + end # 2.5 # 2.5.1 get "#{uri_path}/serviceValidate" do - CASServer::Utils::log_controller_action(self.class, params) + CASServer::Utils::log_controller_action(self.class, params) # force xml content type content_type 'text/xml', :charset => 'utf-8' - # required - @service = clean_service_url(params['service']) - @ticket = params['ticket'] - # optional - @pgt_url = params['pgtUrl'] - @renew = params['renew'] - - st, @error = validate_service_ticket(@service, @ticket) - @success = st && !@error - - if @success - @username = st.username - if @pgt_url - pgt = generate_proxy_granting_ticket(@pgt_url, st) - @pgtiou = pgt.iou if pgt + if ip_allowed?(request.ip) + # required + @service = clean_service_url(params['service']) + @ticket = params['ticket'] + # optional + @pgt_url = params['pgtUrl'] + @renew = params['renew'] + + st, @error = validate_service_ticket(@service, @ticket) + @success = st && !@error + + if @success + @username = st.username + if @pgt_url + pgt = generate_proxy_granting_ticket(@pgt_url, st) + @pgtiou = pgt.iou if pgt + end + @extra_attributes = st.granted_by_tgt.extra_attributes || {} end - @extra_attributes = st.granted_by_tgt.extra_attributes || {} + else + @success = false + @error = Error.new(:INVALID_REQUEST, 'The IP address of this service has not been allowed') end status response_status_from_error(@error) if @error - render :builder, :proxy_validate - end - - + render :builder, :proxy_validate + end + + # 2.6 # 2.6.1 @@ -664,32 +674,38 @@ def self.init_database! # force xml content type content_type 'text/xml', :charset => 'utf-8' - # required - @service = clean_service_url(params['service']) - @ticket = params['ticket'] - # optional - @pgt_url = params['pgtUrl'] - @renew = params['renew'] + if ip_allowed?(request.ip) - @proxies = [] + # required + @service = clean_service_url(params['service']) + @ticket = params['ticket'] + # optional + @pgt_url = params['pgtUrl'] + @renew = params['renew'] - t, @error = validate_proxy_ticket(@service, @ticket) - @success = t && !@error + @proxies = [] - @extra_attributes = {} - if @success - @username = t.username + t, @error = validate_proxy_ticket(@service, @ticket) + @success = t && !@error - if t.kind_of? CASServer::Model::ProxyTicket - @proxies << t.granted_by_pgt.service_ticket.service - end + @extra_attributes = {} + if @success + @username = t.username - if @pgt_url - pgt = generate_proxy_granting_ticket(@pgt_url, t) - @pgtiou = pgt.iou if pgt - end + if t.kind_of? CASServer::Model::ProxyTicket + @proxies << t.granted_by_pgt.service_ticket.service + end - @extra_attributes = t.granted_by_tgt.extra_attributes || {} + if @pgt_url + pgt = generate_proxy_granting_ticket(@pgt_url, t) + @pgtiou = pgt.iou if pgt + end + + @extra_attributes = t.granted_by_tgt.extra_attributes || {} + end + else + @success = false + @error = Error.new(:INVALID_REQUEST, 'The IP address of this service has not been allowed') end status response_status_from_error(@error) if @error @@ -751,5 +767,13 @@ def compile_template(engine, data, options, views) raise unless @custom_views super engine, data, options, views end + + def ip_allowed?(ip) + require 'ipaddr' + + allowed_ips = Array(settings.config[:allowed_service_ips]) + + allowed_ips.empty? || allowed_ips.any? { |i| IPAddr.new(i) === ip } + end end -end +end \ No newline at end of file diff --git a/spec/casserver_spec.rb b/spec/casserver_spec.rb index 2b4cbb34..b8860b82 100644 --- a/spec/casserver_spec.rb +++ b/spec/casserver_spec.rb @@ -141,27 +141,80 @@ end end - describe "proxyValidate" do + describe 'validation' do + let(:allowed_ip) { '127.0.0.1' } + let(:unallowed_ip) { '10.0.0.1' } + let(:service) { @target_service } + before do - load_server("default_config") + load_server('default_config') # 127.0.0.0/24 is allowed here reset_spec_database - visit "/login?service="+CGI.escape(@target_service) + ticket = get_ticket_for(service) - fill_in 'username', :with => VALID_USERNAME - fill_in 'password', :with => VALID_PASSWORD + Rack::Request.any_instance.stub(:ip).and_return(request_ip) + get "/#{path}?service=#{CGI.escape(service)}&ticket=#{CGI.escape(ticket)}" + end - click_button 'login-submit' + subject { last_response } - page.current_url.should =~ /^#{Regexp.escape(@target_service)}\/?\?ticket=ST\-[1-9rA-Z]+/ - @ticket = page.current_url.match(/ticket=(.*)$/)[1] + describe 'validate' do + let(:path) { 'validate' } + + context 'from allowed IP' do + let(:request_ip) { allowed_ip } + + it { should be_ok } + its(:body) { should match 'yes' } + end + + context 'from unallowed IP' do + let(:request_ip) { unallowed_ip } + + its(:status) { should eql 422 } + its(:body) { should match 'no' } + end end - it "should have extra attributes in proper format" do - get "/serviceValidate?service=#{CGI.escape(@target_service)}&ticket=#{@ticket}" + describe 'serviceValidate' do + let(:path) { 'serviceValidate' } - last_response.content_type.should match 'text/xml' - last_response.body.should match "Ютф" + context 'from allowed IP' do + let(:request_ip) { allowed_ip } + + it { should be_ok } + its(:content_type) { should match 'text/xml' } + its(:body) { should match /cas:authenticationSuccess/i } + its(:body) { should match 'Ютф' } + end + + context 'from unallowed IP' do + let(:request_ip) { unallowed_ip } + + its(:status) { should eql 422 } + its(:content_type) { should match 'text/xml' } + its(:body) { should match /cas:authenticationFailure.*INVALID_REQUEST/i } + end + end + + describe 'proxyValidate' do + let(:path) { 'proxyValidate' } + + context 'from allowed IP' do + let(:request_ip) { allowed_ip } + + it { should be_ok } + its(:content_type) { should match 'text/xml' } + its(:body) { should match /cas:authenticationSuccess/i } + end + + context 'from unallowed IP' do + let(:request_ip) { unallowed_ip } + + its(:status) { should eql 422 } + its(:content_type) { should match 'text/xml' } + its(:body) { should match /cas:authenticationFailure.*INVALID_REQUEST/i } + end end end -end +end \ No newline at end of file diff --git a/spec/config/default_config.yml b/spec/config/default_config.yml index 63491480..90a83914 100644 --- a/spec/config/default_config.yml +++ b/spec/config/default_config.yml @@ -48,3 +48,6 @@ enable_single_sign_out: true #maximum_session_lifetime: 172800 #downcase_username: true + +allowed_service_ips: + - 127.0.0.0/24 \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 558e9ec9..8873cd86 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -99,3 +99,12 @@ def reset_spec_database ActiveRecord::Migration.verbose = false ActiveRecord::Migrator.migrate("db/migrate") end + +def get_ticket_for(service, username = 'spec_user', password = 'spec_password') + visit "/login?service=#{CGI.escape(service)}" + fill_in 'username', :with => username + fill_in 'password', :with => password + click_button 'login-submit' + + page.current_url.match(/ticket=(.*)$/)[1] +end \ No newline at end of file