Skip to content

Commit 4b5779b

Browse files
iNecasadamruzicka
authored andcommitted
Fixes #25001 - CVE-2018-14643 - ensure auth (#54)
1 parent bbc2902 commit 4b5779b

2 files changed

Lines changed: 110 additions & 13 deletions

File tree

lib/smart_proxy_dynflow/api.rb

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,53 @@ module Proxy
66
class Dynflow
77
class Api < ::Sinatra::Base
88
helpers ::Proxy::Helpers
9+
helpers ::Proxy::Log
910
helpers ::Proxy::Dynflow::Helpers
1011

1112
before do
12-
logger = Proxy::LogBuffer::Decorator.instance
1313
content_type :json
1414
if request.env['HTTP_AUTHORIZATION'] && request.env['PATH_INFO'].end_with?('/done')
1515
# Halt running before callbacks if a token is provided and the request is notifying about task being done
1616
return
17+
else
18+
do_authorize_with_ssl_client
19+
do_authorize_with_trusted_hosts
1720
end
1821
end
1922

20-
helpers Sinatra::Authorization
23+
24+
# TODO: move this to foreman-proxy to reduce code duplicities
25+
def do_authorize_with_trusted_hosts
26+
# When :trusted_hosts is given, we check the client against the list
27+
# HTTPS: test the certificate CN
28+
# HTTP: test the reverse DNS entry of the remote IP
29+
trusted_hosts = Proxy::SETTINGS.trusted_hosts
30+
if trusted_hosts
31+
if [ 'yes', 'on', 1 ].include? request.env['HTTPS'].to_s
32+
fqdn = https_cert_cn
33+
source = 'SSL_CLIENT_CERT'
34+
else
35+
fqdn = remote_fqdn(Proxy::SETTINGS.forward_verify)
36+
source = 'REMOTE_ADDR'
37+
end
38+
fqdn = fqdn.downcase
39+
logger.debug "verifying remote client #{fqdn} (based on #{source}) against trusted_hosts #{trusted_hosts}"
40+
41+
unless Proxy::SETTINGS.trusted_hosts.include?(fqdn)
42+
log_halt 403, "Untrusted client #{fqdn} attempted to access #{request.path_info}. Check :trusted_hosts: in settings.yml"
43+
end
44+
end
45+
end
46+
47+
def do_authorize_with_ssl_client
48+
if ['yes', 'on', '1'].include? request.env['HTTPS'].to_s
49+
if request.env['SSL_CLIENT_CERT'].to_s.empty?
50+
log_halt 403, "No client SSL certificate supplied"
51+
end
52+
else
53+
logger.debug('require_ssl_client_verification: skipping, non-HTTPS request')
54+
end
55+
end
2156

2257
post "/*" do
2358
relay_request

test/api_test/api_test.rb

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,34 @@ def hostname
1414
'somehost.somedomain.org:9000'
1515
end
1616

17-
def request_factory(kind, path)
17+
def request_factory(kind, path, env = {})
1818
body = mock()
1919
body.stubs(:read).returns("")
20-
env = {
20+
env = env.merge(
2121
'REQUEST_METHOD' => kind,
2222
'rack.request.query_hash' => {},
23-
'HTTP_HOST' => hostname
24-
}
25-
OpenStruct.new(:env => env, :body => body, :path => '/dynflow' + path)
23+
'HTTP_HOST' => hostname,
24+
'PATH_INFO' => "/dynflow#{path}"
25+
)
26+
Sinatra::Request.new(env).tap do |r|
27+
r.stubs(:body).returns(body)
28+
end
2629
end
2730

2831
let(:new_request) { Net::HTTP::Get.new 'example.org' }
2932

30-
it 'relays GET requests' do
33+
def mock_core_service(method, path, response)
3134
factory = mock()
32-
factory.expects(:create_get).with('/tasks/count', {}).returns(new_request)
35+
factory.expects(method).with { |p| p == path }.returns(new_request)
3336
Proxy::Dynflow::Callback::Core.any_instance.expects(:request_factory).returns(factory)
3437
Proxy::Dynflow::Callback::Core.any_instance
35-
.expects(:send_request).with(new_request)
36-
.returns(OpenStruct.new(:code => 200, :body => {'count' => 0}))
37-
Sinatra::Base.any_instance.expects(:request).times(4).returns(request_factory('GET', '/tasks/count'))
38+
.expects(:send_request).with(new_request)
39+
.returns(OpenStruct.new(response))
40+
end
41+
42+
it 'relays GET requests' do
43+
mock_core_service(:create_get, '/tasks/count', :code => 200, :body => {'count' => 0})
44+
Proxy::Dynflow::Api.any_instance.stubs(:request).returns(request_factory('GET', '/tasks/count'))
3845
get '/tasks/count'
3946
new_request['X-Forwarded-For'].must_equal hostname
4047
end
@@ -46,9 +53,64 @@ def request_factory(kind, path)
4653
Proxy::Dynflow::Callback::Core.any_instance
4754
.expects(:send_request).with(new_request)
4855
.returns(OpenStruct.new(:code => 200, :body => {'count' => 0}))
49-
Sinatra::Base.any_instance.expects(:request).times(4).returns(request_factory('POST', '/tasks/12345/cancel'))
56+
Proxy::Dynflow::Api.any_instance.stubs(:request).returns(request_factory('POST', '/tasks/12345/cancel'))
5057
post '/tasks/12345/cancel', {}
5158
new_request['X-Forwarded-For'].must_equal hostname
5259
end
60+
61+
it 'refuses unauthorized http connections (using remote_fqdn)' do
62+
Proxy::Dynflow::Api.any_instance.stubs(:request).returns(request_factory('POST', '/tasks'))
63+
Proxy::Dynflow::Api.any_instance.stubs(:remote_fqdn).returns('unauthorized_host.example.com')
64+
Proxy::SETTINGS.stubs(:trusted_hosts).returns(["mytrustedhost.example.com"])
65+
post '/tasks'
66+
assert(last_response.forbidden?, 'The request should be forbidden')
67+
end
68+
69+
it 'accepts authorized http connections (using remote_fqdn)' do
70+
mock_core_service(:create_post, '/tasks', :code => 200, :body => {})
71+
Proxy::Dynflow::Api.any_instance.stubs(:request).returns(request_factory('POST', '/tasks'))
72+
Proxy::Dynflow::Api.any_instance.stubs(:remote_fqdn).returns('mytrustedhost.example.com')
73+
Proxy::SETTINGS.stubs(:trusted_hosts).returns(["mytrustedhost.example.com"])
74+
post '/tasks'
75+
assert(last_response.ok?, 'The response should be ok')
76+
end
77+
78+
it 'refuses unauthorized https connections (using https_cert_cn)' do
79+
Proxy::Dynflow::Api.any_instance.stubs(:request).
80+
returns(request_factory('POST', '/tasks', 'HTTPS' => 'yes','SSL_CLIENT_CERT' => 'mytrustedcert'))
81+
Proxy::Dynflow::Api.any_instance.stubs(:https_cert_cn).returns('unauthorized_host.example.com')
82+
Proxy::SETTINGS.stubs(:trusted_hosts).returns(["mytrustedhost.example.com"])
83+
post '/tasks'
84+
assert(last_response.forbidden?, 'The request should be forbidden')
85+
end
86+
87+
it 'accepts unauthorized https connections (using https_cert_cn)' do
88+
mock_core_service(:create_post, '/tasks', :code => 200, :body => {})
89+
Proxy::Dynflow::Api.any_instance.stubs(:request).
90+
returns(request_factory('POST', '/tasks', 'HTTPS' => 'yes', 'SSL_CLIENT_CERT' => 'mytrustedcert'))
91+
Proxy::Dynflow::Api.any_instance.stubs(:https_cert_cn).returns('mytrustedhost.example.com')
92+
Proxy::SETTINGS.stubs(:trusted_hosts).returns(["mytrustedhost.example.com"])
93+
post '/tasks'
94+
assert(last_response.ok?, 'The response should be ok')
95+
end
96+
97+
it 'refuses unauthorized https connections (when client cert is not supplied)' do
98+
Proxy::Dynflow::Api.any_instance.stubs(:request).
99+
returns(request_factory('POST', '/tasks', 'HTTPS' => 'yes'))
100+
Proxy::Dynflow::Api.any_instance.stubs(:https_cert_cn).returns('mytrustedhost.example.com')
101+
Proxy::SETTINGS.stubs(:trusted_hosts).returns(["mytrustedhost.example.com"])
102+
post '/tasks'
103+
assert(last_response.forbidden?, 'The request should be forbidden')
104+
end
105+
106+
it 'passes the done requests to the core service, when authorization keys are provided' do
107+
mock_core_service(:create_post, '/tasks/123/done', :code => 200, :body => {})
108+
Proxy::Dynflow::Api.any_instance.stubs(:request).
109+
returns(request_factory('POST', '/tasks/123/done', 'HTTPS' => 'yes', 'HTTP_AUTHORIZATION' => 'Basic ValidToken'))
110+
Proxy::Dynflow::Api.any_instance.stubs(:https_cert_cn).returns('mytrustedhost.example.com')
111+
Proxy::SETTINGS.stubs(:trusted_hosts).returns(["mytrustedhost.example.com"])
112+
post '/tasks'
113+
assert(last_response.ok?, 'The response should be ok')
114+
end
53115
end
54116
end

0 commit comments

Comments
 (0)