Skip to content

Commit 658fa17

Browse files
committed
Fixes #39817 - Respect configured smart proxy names as Sinatra permitted_hosts
Warn when permitted hosts are not enforced by Sinatra
1 parent c2af3d3 commit 658fa17

6 files changed

Lines changed: 102 additions & 0 deletions

File tree

‎config/settings.yml.example‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
# Valid values: '1.0', '1.1', '1.2', '1.3'.
1919
#:tls_min_version: '1.2'
2020

21+
# Hostnames accepted in HTTP Host headers. An empty list allows any hostname.
22+
# This is unrelated to trusted_hosts, which authorizes client machines.
23+
#:permitted_hosts:
24+
# - proxy.example.com
25+
2126
# Hosts which the proxy accepts connections from
2227
# commenting the following lines would mean every verified SSL connection allowed
2328
# HTTPS: test the certificate CN

‎lib/launcher.rb‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ def launch
219219
raise Exception.new("Both http and https are disabled, unable to start.") unless http_enabled? || https_enabled?
220220

221221
::Proxy::PluginInitializer.new(::Proxy::Plugins.instance).initialize_plugins
222+
warn_if_unsupported_host_authorization
222223

223224
http_app = http_app(settings.http_port)
224225
https_app = https_app(settings.https_port)
@@ -242,6 +243,14 @@ def launch
242243
exit(1)
243244
end
244245

246+
def warn_if_unsupported_host_authorization
247+
return unless settings.permitted_hosts&.any?
248+
# Sinatra 2 and 3 accept the setting but have no host authorization middleware.
249+
return if ::Sinatra::Base.respond_to?(:setup_host_authorization, true)
250+
251+
logger.warn("permitted_hosts is configured but not enforced by this Sinatra version")
252+
end
253+
245254
def install_webrick_callback!(*apps)
246255
apps.compact!
247256

‎lib/smart_proxy_main.rb‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ module Proxy
5151
# requests whose Host header isn't localhost — breaking all real
5252
# deployments where Foreman reaches smart-proxy by hostname.
5353
::Sinatra::Base.set :environment, :production
54+
# Sinatra 2 ignores this setting; without configured hosts, Sinatra 4
55+
# accepts all hostnames as it did before.
56+
# Sinatra passes this hash to Rack::Protection::HostAuthorization, which runs
57+
# before LoggerMiddleware. Use the proxy logger for rejected-host diagnostics.
58+
::Sinatra::Base.set :host_authorization, {
59+
permitted_hosts: SETTINGS.permitted_hosts || [],
60+
logger: ::Proxy::LogBuffer::Decorator.instance,
61+
}
5462
::Sinatra::Base.register ::Sinatra::Authorization
5563

5664
require 'root/root'

‎test/global_settings_test.rb‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'test_helper'
2+
require 'tempfile'
23

34
class GlobalSettingsTest < Test::Unit::TestCase
45
def test_default_values
@@ -9,6 +10,28 @@ def test_default_values
910
assert_equal "INFO", settings.log_level
1011
end
1112

13+
def test_reads_permitted_hosts_from_config
14+
settings = read_global_settings(<<~YAML)
15+
:permitted_hosts:
16+
- proxy.example.com
17+
- alias.example.com
18+
YAML
19+
20+
assert_equal ['proxy.example.com', 'alias.example.com'], settings.permitted_hosts
21+
end
22+
23+
def test_reads_empty_permitted_hosts_from_config
24+
settings = read_global_settings(":permitted_hosts: []\n")
25+
26+
assert_equal [], settings.permitted_hosts
27+
end
28+
29+
def test_permitted_hosts_is_nil_when_unset_in_config
30+
settings = read_global_settings("{}\n")
31+
32+
assert_nil settings.permitted_hosts
33+
end
34+
1235
def test_normalize_setting
1336
how_to = { :test => ->(value) { value + 1 } }
1437
assert_equal 2, ::Proxy::Settings::Global.new({}).normalize_setting(:test, 1, how_to)
@@ -26,4 +49,14 @@ def test_bind_host_is_normalized
2649
assert_equal ['127.0.0.1'], ::Proxy::Settings::Global.new(:bind_host => '127.0.0.1').bind_host
2750
assert_equal ['127.0.0.1'], ::Proxy::Settings::Global.new(:bind_host => ['127.0.0.1']).bind_host
2851
end
52+
53+
private
54+
55+
def read_global_settings(yaml)
56+
Tempfile.create('smart-proxy-settings') do |file|
57+
file.write(yaml)
58+
file.flush
59+
::Proxy::Settings.initialize_global_settings(file.path)
60+
end
61+
end
2962
end

‎test/launcher_test.rb‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,34 @@ def test_launched_with_sdnotify
2323
end
2424
end
2525

26+
class LauncherHostAuthorizationTest < Test::Unit::TestCase
27+
def test_warns_when_permitted_hosts_are_not_enforced
28+
settings = Proxy::Settings::Global.new(permitted_hosts: ['proxy.example.com'])
29+
launcher = Proxy::Launcher.new(settings)
30+
::Sinatra::Base.expects(:respond_to?).with(:setup_host_authorization, true).returns(false)
31+
launcher.logger.expects(:warn).with('permitted_hosts is configured but not enforced by this Sinatra version')
32+
33+
launcher.warn_if_unsupported_host_authorization
34+
end
35+
36+
def test_no_warning_when_host_authorization_is_supported
37+
settings = Proxy::Settings::Global.new(permitted_hosts: ['proxy.example.com'])
38+
launcher = Proxy::Launcher.new(settings)
39+
::Sinatra::Base.expects(:respond_to?).with(:setup_host_authorization, true).returns(true)
40+
launcher.logger.expects(:warn).never
41+
42+
launcher.warn_if_unsupported_host_authorization
43+
end
44+
45+
def test_no_warning_when_permitted_hosts_are_empty
46+
settings = Proxy::Settings::Global.new(permitted_hosts: [])
47+
launcher = Proxy::Launcher.new(settings)
48+
launcher.logger.expects(:warn).never
49+
50+
launcher.warn_if_unsupported_host_authorization
51+
end
52+
end
53+
2654
class LauncherTlsCiphersTest < Test::Unit::TestCase
2755
def setup
2856
@launcher = Proxy::Launcher.new

‎test/root/root_api_test.rb‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,25 @@ def test_features
3333
assert_equal ['test2'], JSON.parse(last_response.body)
3434
end
3535

36+
def test_features_rejects_unpermitted_host
37+
omit 'This Sinatra version has no host authorization' unless ::Sinatra::Base.respond_to?(:setup_host_authorization, true)
38+
39+
::Proxy::RootApi.stubs(:host_authorization).returns(permitted_hosts: ['proxy.example.com'])
40+
get '/features', {}, 'HTTP_HOST' => 'proxy.example.com'
41+
assert_equal 200, last_response.status
42+
43+
get '/features', {}, 'HTTP_HOST' => 'unknown.example.com'
44+
assert_equal 403, last_response.status
45+
assert_equal 'Host not permitted', last_response.body
46+
end
47+
48+
def test_features_accepts_any_host_when_permitted_hosts_are_empty
49+
::Proxy::RootApi.stubs(:host_authorization).returns(permitted_hosts: [])
50+
51+
get '/features', {}, 'HTTP_HOST' => 'unknown.example.com'
52+
assert_equal 200, last_response.status
53+
end
54+
3655
def test_version
3756
all_modules = [{:name => :foreman_proxy, :version => "0.0.1", :class => TestPlugin0, :state => :running},
3857
{:name => :test2, :version => "0.0.1", :class => TestPlugin2, :state => :running},

0 commit comments

Comments
 (0)