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.
This commit is contained in:
Adam Crownoble 2012-12-12 22:09:24 -08:00
Родитель 3c0c0dba92
Коммит 1be41b92d9
5 изменённых файлов: 171 добавлений и 73 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -601,60 +601,70 @@ module CASServer
# 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']
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
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 @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 @@ module CASServer
# 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
@extra_attributes = {}
if @success
@username = t.username
if t.kind_of? CASServer::Model::ProxyTicket
@proxies << t.granted_by_pgt.service_ticket.service
end
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
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 || {}
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 @@ module CASServer
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

Просмотреть файл

@ -141,27 +141,80 @@ describe 'CASServer' do
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
click_button 'login-submit'
page.current_url.should =~ /^#{Regexp.escape(@target_service)}\/?\?ticket=ST\-[1-9rA-Z]+/
@ticket = page.current_url.match(/ticket=(.*)$/)[1]
Rack::Request.any_instance.stub(:ip).and_return(request_ip)
get "/#{path}?service=#{CGI.escape(service)}&ticket=#{CGI.escape(ticket)}"
end
it "should have extra attributes in proper format" do
get "/serviceValidate?service=#{CGI.escape(@target_service)}&ticket=#{@ticket}"
subject { last_response }
last_response.content_type.should match 'text/xml'
last_response.body.should match "<test_utf_string>Ютф</test_utf_string>"
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
describe 'serviceValidate' do
let(:path) { 'serviceValidate' }
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 '<test_utf_string>Ютф</test_utf_string>' }
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

Просмотреть файл

@ -48,3 +48,6 @@ enable_single_sign_out: true
#maximum_session_lifetime: 172800
#downcase_username: true
allowed_service_ips:
- 127.0.0.0/24

Просмотреть файл

@ -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