From 95f1d47bb54cf18b3c8cdf8d36c261b977afd017 Mon Sep 17 00:00:00 2001 From: Mike Dorman Date: Tue, 14 Jul 2015 11:25:50 -0600 Subject: [PATCH] Add console allowed origins setting Adds a DEFAULT/console_allowed_origins to the console websocket proxy to allow connections from other Origin hostnames (other than only the hostname in the Host header.) Origin header checking was introduced in https://review.openstack.org/#/c/163033/ as a fix for bug 1409142 and CVE-2015-0259. However there are valid use cases where you want to do cross-site web socket connections. This patch allows for controlled cross-site web socket connections to the console proxy services. DocImpact Change-Id: If7995bb7afc255f5ad834dc8a7044ef38b6cb335 Closes-bug: 1474079 --- nova/console/websocketproxy.py | 14 ++++++++- .../tests/unit/console/test_websocketproxy.py | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/nova/console/websocketproxy.py b/nova/console/websocketproxy.py index c64ce8eeb584..435cc098be51 100644 --- a/nova/console/websocketproxy.py +++ b/nova/console/websocketproxy.py @@ -30,9 +30,19 @@ from nova.consoleauth import rpcapi as consoleauth_rpcapi from nova import context from nova import exception from nova.i18n import _ +from oslo_config import cfg LOG = logging.getLogger(__name__) +CONF = cfg.CONF +console_origin_opts = [ + cfg.ListOpt('console_allowed_origins', + default=[], + help='Allowed Origin header hostnames for access to console ' + 'proxy servers'), +] +CONF.register_opts(console_origin_opts) + class NovaProxyRequestHandlerBase(object): def address_string(self): @@ -104,6 +114,8 @@ class NovaProxyRequestHandlerBase(object): expected_origin_hostname = e.split(']')[0][1:] else: expected_origin_hostname = e.split(':')[0] + expected_origin_hostnames = CONF.console_allowed_origins + expected_origin_hostnames.append(expected_origin_hostname) origin_url = self.headers.getheader('Origin') # missing origin header indicates non-browser client which is OK if origin_url is not None: @@ -113,7 +125,7 @@ class NovaProxyRequestHandlerBase(object): if origin_hostname == '' or origin_scheme == '': detail = _("Origin header not valid.") raise exception.ValidationError(detail=detail) - if expected_origin_hostname != origin_hostname: + if origin_hostname not in expected_origin_hostnames: detail = _("Origin header does not match this host.") raise exception.ValidationError(detail=detail) if not self.verify_origin_proto(connect_info, origin_scheme): diff --git a/nova/tests/unit/console/test_websocketproxy.py b/nova/tests/unit/console/test_websocketproxy.py index b5fbe2488470..a6b20650e636 100644 --- a/nova/tests/unit/console/test_websocketproxy.py +++ b/nova/tests/unit/console/test_websocketproxy.py @@ -30,6 +30,8 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase): def setUp(self): super(NovaProxyRequestHandlerBaseTestCase, self).setUp() + self.flags(console_allowed_origins = ['allowed-origin-example-1.net', + 'allowed-origin-example-2.net']) self.wh = websocketproxy.NovaProxyRequestHandlerBase() self.wh.socket = mock.MagicMock() self.wh.msg = mock.MagicMock() @@ -76,6 +78,16 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase): else: return + def _fake_getheader_allowed_origin(self, header): + if header == 'cookie': + return 'token="123-456-789"' + elif header == 'Origin': + return 'https://allowed-origin-example-2.net:6080' + elif header == 'Host': + return 'example.net:6080' + else: + return + def _fake_getheader_blank_origin(self, header): if header == 'cookie': return 'token="123-456-789"' @@ -266,6 +278,25 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase): self.assertRaises(exception.ValidationError, self.wh.new_websocket_client) + @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token') + def test_new_websocket_client_novnc_allowed_origin_header(self, + check_token): + check_token.return_value = { + 'host': 'node1', + 'port': '10000', + 'console_type': 'novnc', + 'access_url': 'https://example.net:6080' + } + self.wh.socket.return_value = '' + self.wh.path = "http://127.0.0.1/" + self.wh.headers.getheader = self._fake_getheader_allowed_origin + + self.wh.new_websocket_client() + + check_token.assert_called_with(mock.ANY, token="123-456-789") + self.wh.socket.assert_called_with('node1', 10000, connect=True) + self.wh.do_proxy.assert_called_with('') + @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token') def test_new_websocket_client_novnc_blank_origin_header(self, check_token): check_token.return_value = {