Skip to content

Commit 1be41b9

Browse files
author
Adam Crownoble
committed
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.
1 parent 3c0c0db commit 1be41b9

File tree

5 files changed

+170
-72
lines changed

5 files changed

+170
-72
lines changed

config/config.example.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,3 +538,12 @@ log:
538538
# convert this to "jsmith".
539539

540540
#downcase_username: true
541+
542+
# If you'd like to limit the service hosts that can use CAS for authentication,
543+
# add the individual IPs and IP ranges in CIDR notation below. Leaving this
544+
# setting blank will allow any server to authenticate users via the CAS server
545+
# and potentially harvest sensitive user information.
546+
547+
#allowed_service_ips:
548+
# - 127.0.0.1
549+
# - 192.168.0.0/24

lib/casserver/server.rb

Lines changed: 83 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -601,60 +601,70 @@ def self.init_database!
601601
# 2.4
602602

603603
# 2.4.1
604-
get "#{uri_path}/validate" do
605-
CASServer::Utils::log_controller_action(self.class, params)
606-
607-
# required
608-
@service = clean_service_url(params['service'])
609-
@ticket = params['ticket']
610-
# optional
611-
@renew = params['renew']
612-
613-
st, @error = validate_service_ticket(@service, @ticket)
614-
@success = st && !@error
615-
616-
@username = st.username if @success
617-
604+
get "#{uri_path}/validate" do
605+
CASServer::Utils::log_controller_action(self.class, params)
606+
607+
if ip_allowed?(request.ip)
608+
# required
609+
@service = clean_service_url(params['service'])
610+
@ticket = params['ticket']
611+
# optional
612+
@renew = params['renew']
613+
614+
st, @error = validate_service_ticket(@service, @ticket)
615+
@success = st && !@error
616+
617+
@username = st.username if @success
618+
else
619+
@success = false
620+
@error = Error.new(:INVALID_REQUEST, 'The IP address of this service has not been allowed')
621+
end
622+
618623
status response_status_from_error(@error) if @error
619-
620-
render @template_engine, :validate, :layout => false
621-
end
624+
625+
render @template_engine, :validate, :layout => false
626+
end
622627

623628

624629
# 2.5
625630

626631
# 2.5.1
627632
get "#{uri_path}/serviceValidate" do
628-
CASServer::Utils::log_controller_action(self.class, params)
633+
CASServer::Utils::log_controller_action(self.class, params)
629634

630635
# force xml content type
631636
content_type 'text/xml', :charset => 'utf-8'
632637

633-
# required
634-
@service = clean_service_url(params['service'])
635-
@ticket = params['ticket']
636-
# optional
637-
@pgt_url = params['pgtUrl']
638-
@renew = params['renew']
639-
640-
st, @error = validate_service_ticket(@service, @ticket)
641-
@success = st && !@error
642-
643-
if @success
644-
@username = st.username
645-
if @pgt_url
646-
pgt = generate_proxy_granting_ticket(@pgt_url, st)
647-
@pgtiou = pgt.iou if pgt
638+
if ip_allowed?(request.ip)
639+
# required
640+
@service = clean_service_url(params['service'])
641+
@ticket = params['ticket']
642+
# optional
643+
@pgt_url = params['pgtUrl']
644+
@renew = params['renew']
645+
646+
st, @error = validate_service_ticket(@service, @ticket)
647+
@success = st && !@error
648+
649+
if @success
650+
@username = st.username
651+
if @pgt_url
652+
pgt = generate_proxy_granting_ticket(@pgt_url, st)
653+
@pgtiou = pgt.iou if pgt
654+
end
655+
@extra_attributes = st.granted_by_tgt.extra_attributes || {}
648656
end
649-
@extra_attributes = st.granted_by_tgt.extra_attributes || {}
657+
else
658+
@success = false
659+
@error = Error.new(:INVALID_REQUEST, 'The IP address of this service has not been allowed')
650660
end
651661

652662
status response_status_from_error(@error) if @error
653663

654-
render :builder, :proxy_validate
655-
end
656-
657-
664+
render :builder, :proxy_validate
665+
end
666+
667+
658668
# 2.6
659669

660670
# 2.6.1
@@ -664,32 +674,38 @@ def self.init_database!
664674
# force xml content type
665675
content_type 'text/xml', :charset => 'utf-8'
666676

667-
# required
668-
@service = clean_service_url(params['service'])
669-
@ticket = params['ticket']
670-
# optional
671-
@pgt_url = params['pgtUrl']
672-
@renew = params['renew']
677+
if ip_allowed?(request.ip)
673678

674-
@proxies = []
679+
# required
680+
@service = clean_service_url(params['service'])
681+
@ticket = params['ticket']
682+
# optional
683+
@pgt_url = params['pgtUrl']
684+
@renew = params['renew']
675685

676-
t, @error = validate_proxy_ticket(@service, @ticket)
677-
@success = t && !@error
686+
@proxies = []
678687

679-
@extra_attributes = {}
680-
if @success
681-
@username = t.username
688+
t, @error = validate_proxy_ticket(@service, @ticket)
689+
@success = t && !@error
682690

683-
if t.kind_of? CASServer::Model::ProxyTicket
684-
@proxies << t.granted_by_pgt.service_ticket.service
685-
end
691+
@extra_attributes = {}
692+
if @success
693+
@username = t.username
686694

687-
if @pgt_url
688-
pgt = generate_proxy_granting_ticket(@pgt_url, t)
689-
@pgtiou = pgt.iou if pgt
690-
end
695+
if t.kind_of? CASServer::Model::ProxyTicket
696+
@proxies << t.granted_by_pgt.service_ticket.service
697+
end
691698

692-
@extra_attributes = t.granted_by_tgt.extra_attributes || {}
699+
if @pgt_url
700+
pgt = generate_proxy_granting_ticket(@pgt_url, t)
701+
@pgtiou = pgt.iou if pgt
702+
end
703+
704+
@extra_attributes = t.granted_by_tgt.extra_attributes || {}
705+
end
706+
else
707+
@success = false
708+
@error = Error.new(:INVALID_REQUEST, 'The IP address of this service has not been allowed')
693709
end
694710

695711
status response_status_from_error(@error) if @error
@@ -751,5 +767,13 @@ def compile_template(engine, data, options, views)
751767
raise unless @custom_views
752768
super engine, data, options, views
753769
end
770+
771+
def ip_allowed?(ip)
772+
require 'ipaddr'
773+
774+
allowed_ips = Array(settings.config[:allowed_service_ips])
775+
776+
allowed_ips.empty? || allowed_ips.any? { |i| IPAddr.new(i) === ip }
777+
end
754778
end
755-
end
779+
end

spec/casserver_spec.rb

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,27 +141,80 @@
141141
end
142142
end
143143

144-
describe "proxyValidate" do
144+
describe 'validation' do
145+
let(:allowed_ip) { '127.0.0.1' }
146+
let(:unallowed_ip) { '10.0.0.1' }
147+
let(:service) { @target_service }
148+
145149
before do
146-
load_server("default_config")
150+
load_server('default_config') # 127.0.0.0/24 is allowed here
147151
reset_spec_database
148152

149-
visit "/login?service="+CGI.escape(@target_service)
153+
ticket = get_ticket_for(service)
150154

151-
fill_in 'username', :with => VALID_USERNAME
152-
fill_in 'password', :with => VALID_PASSWORD
155+
Rack::Request.any_instance.stub(:ip).and_return(request_ip)
156+
get "/#{path}?service=#{CGI.escape(service)}&ticket=#{CGI.escape(ticket)}"
157+
end
153158

154-
click_button 'login-submit'
159+
subject { last_response }
155160

156-
page.current_url.should =~ /^#{Regexp.escape(@target_service)}\/?\?ticket=ST\-[1-9rA-Z]+/
157-
@ticket = page.current_url.match(/ticket=(.*)$/)[1]
161+
describe 'validate' do
162+
let(:path) { 'validate' }
163+
164+
context 'from allowed IP' do
165+
let(:request_ip) { allowed_ip }
166+
167+
it { should be_ok }
168+
its(:body) { should match 'yes' }
169+
end
170+
171+
context 'from unallowed IP' do
172+
let(:request_ip) { unallowed_ip }
173+
174+
its(:status) { should eql 422 }
175+
its(:body) { should match 'no' }
176+
end
158177
end
159178

160-
it "should have extra attributes in proper format" do
161-
get "/serviceValidate?service=#{CGI.escape(@target_service)}&ticket=#{@ticket}"
179+
describe 'serviceValidate' do
180+
let(:path) { 'serviceValidate' }
162181

163-
last_response.content_type.should match 'text/xml'
164-
last_response.body.should match "<test_utf_string>Ютф</test_utf_string>"
182+
context 'from allowed IP' do
183+
let(:request_ip) { allowed_ip }
184+
185+
it { should be_ok }
186+
its(:content_type) { should match 'text/xml' }
187+
its(:body) { should match /cas:authenticationSuccess/i }
188+
its(:body) { should match '<test_utf_string>Ютф</test_utf_string>' }
189+
end
190+
191+
context 'from unallowed IP' do
192+
let(:request_ip) { unallowed_ip }
193+
194+
its(:status) { should eql 422 }
195+
its(:content_type) { should match 'text/xml' }
196+
its(:body) { should match /cas:authenticationFailure.*INVALID_REQUEST/i }
197+
end
198+
end
199+
200+
describe 'proxyValidate' do
201+
let(:path) { 'proxyValidate' }
202+
203+
context 'from allowed IP' do
204+
let(:request_ip) { allowed_ip }
205+
206+
it { should be_ok }
207+
its(:content_type) { should match 'text/xml' }
208+
its(:body) { should match /cas:authenticationSuccess/i }
209+
end
210+
211+
context 'from unallowed IP' do
212+
let(:request_ip) { unallowed_ip }
213+
214+
its(:status) { should eql 422 }
215+
its(:content_type) { should match 'text/xml' }
216+
its(:body) { should match /cas:authenticationFailure.*INVALID_REQUEST/i }
217+
end
165218
end
166219
end
167-
end
220+
end

spec/config/default_config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,6 @@ enable_single_sign_out: true
4848
#maximum_session_lifetime: 172800
4949

5050
#downcase_username: true
51+
52+
allowed_service_ips:
53+
- 127.0.0.0/24

spec/spec_helper.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,12 @@ def reset_spec_database
9999
ActiveRecord::Migration.verbose = false
100100
ActiveRecord::Migrator.migrate("db/migrate")
101101
end
102+
103+
def get_ticket_for(service, username = 'spec_user', password = 'spec_password')
104+
visit "/login?service=#{CGI.escape(service)}"
105+
fill_in 'username', :with => username
106+
fill_in 'password', :with => password
107+
click_button 'login-submit'
108+
109+
page.current_url.match(/ticket=(.*)$/)[1]
110+
end

0 commit comments

Comments
 (0)