components/openstack/nova/patches/07-CVE-2015-0259.patch
changeset 5405 66fd59fecd68
parent 5404 55e409ba4e72
child 5406 5ac656f02914
equal deleted inserted replaced
5404:55e409ba4e72 5405:66fd59fecd68
     1 Upstream patch to address CVE-2015-0259.  This fix will be included in
       
     2 the future 2014.2.3 (juno) release.
       
     3 
       
     4 From 676ba7bbc788a528b0fe4c87c1c4bf94b4bb6eb1 Mon Sep 17 00:00:00 2001
       
     5 From: Dave McCowan <[email protected]>
       
     6 Date: Tue, 24 Feb 2015 21:35:48 -0500
       
     7 Subject: [PATCH] Websocket Proxy should verify Origin header
       
     8 
       
     9 If the Origin HTTP header passed in the WebSocket handshake does
       
    10 not match the host, this could indicate an attempt at a
       
    11 cross-site attack.  This commit adds a check to verify
       
    12 the origin matches the host.
       
    13 
       
    14 Change-Id: Ica6ec23d6f69a236657d5ba0c3f51b693c633649
       
    15 Closes-Bug: 1409142
       
    16 ---
       
    17  nova/console/websocketproxy.py            |   45 +++++++
       
    18  nova/tests/console/test_websocketproxy.py |  185 ++++++++++++++++++++++++++++-
       
    19  2 files changed, 226 insertions(+), 4 deletions(-)
       
    20 
       
    21 diff --git a/nova/console/websocketproxy.py b/nova/console/websocketproxy.py
       
    22 index ef684f5..7a1e056 100644
       
    23 --- a/nova/console/websocketproxy.py
       
    24 +++ b/nova/console/websocketproxy.py
       
    25 @@ -22,17 +22,40 @@ import Cookie
       
    26  import socket
       
    27  import urlparse
       
    28  
       
    29 +from oslo.config import cfg
       
    30  import websockify
       
    31  
       
    32  from nova.consoleauth import rpcapi as consoleauth_rpcapi
       
    33  from nova import context
       
    34 +from nova import exception
       
    35  from nova.i18n import _
       
    36  from nova.openstack.common import log as logging
       
    37  
       
    38  LOG = logging.getLogger(__name__)
       
    39  
       
    40 +CONF = cfg.CONF
       
    41 +CONF.import_opt('novncproxy_base_url', 'nova.vnc')
       
    42 +CONF.import_opt('html5proxy_base_url', 'nova.spice', group='spice')
       
    43 +CONF.import_opt('base_url', 'nova.console.serial', group='serial_console')
       
    44 +
       
    45  
       
    46  class NovaProxyRequestHandlerBase(object):
       
    47 +    def verify_origin_proto(self, console_type, origin_proto):
       
    48 +        if console_type == 'novnc':
       
    49 +            expected_proto = \
       
    50 +                urlparse.urlparse(CONF.novncproxy_base_url).scheme
       
    51 +        elif console_type == 'spice-html5':
       
    52 +            expected_proto = \
       
    53 +                urlparse.urlparse(CONF.spice.html5proxy_base_url).scheme
       
    54 +        elif console_type == 'serial':
       
    55 +            expected_proto = \
       
    56 +                urlparse.urlparse(CONF.serial_console.base_url).scheme
       
    57 +        else:
       
    58 +            detail = _("Invalid Console Type for WebSocketProxy: '%s'") % \
       
    59 +                        console_type
       
    60 +            raise exception.ValidationError(detail=detail)
       
    61 +        return origin_proto == expected_proto
       
    62 +
       
    63      def new_websocket_client(self):
       
    64          """Called after a new WebSocket connection has been established."""
       
    65          # Reopen the eventlet hub to make sure we don't share an epoll
       
    66 @@ -62,6 +85,28 @@ class NovaProxyRequestHandlerBase(object):
       
    67          if not connect_info:
       
    68              raise Exception(_("Invalid Token"))
       
    69  
       
    70 +        # Verify Origin
       
    71 +        expected_origin_hostname = self.headers.getheader('Host')
       
    72 +        if ':' in expected_origin_hostname:
       
    73 +            e = expected_origin_hostname
       
    74 +            expected_origin_hostname = e.split(':')[0]
       
    75 +        origin_url = self.headers.getheader('Origin')
       
    76 +        # missing origin header indicates non-browser client which is OK
       
    77 +        if origin_url is not None:
       
    78 +            origin = urlparse.urlparse(origin_url)
       
    79 +            origin_hostname = origin.hostname
       
    80 +            origin_scheme = origin.scheme
       
    81 +            if origin_hostname == '' or origin_scheme == '':
       
    82 +                detail = _("Origin header not valid.")
       
    83 +                raise exception.ValidationError(detail=detail)
       
    84 +            if expected_origin_hostname != origin_hostname:
       
    85 +                detail = _("Origin header does not match this host.")
       
    86 +                raise exception.ValidationError(detail=detail)
       
    87 +            if not self.verify_origin_proto(connect_info['console_type'],
       
    88 +                                              origin.scheme):
       
    89 +                detail = _("Origin header protocol does not match this host.")
       
    90 +                raise exception.ValidationError(detail=detail)
       
    91 +
       
    92          self.msg(_('connect info: %s'), str(connect_info))
       
    93          host = connect_info['host']
       
    94          port = int(connect_info['port'])
       
    95 diff --git a/nova/tests/console/test_websocketproxy.py b/nova/tests/console/test_websocketproxy.py
       
    96 index 1e51a4d..66913c2 100644
       
    97 --- a/nova/tests/console/test_websocketproxy.py
       
    98 +++ b/nova/tests/console/test_websocketproxy.py
       
    99 @@ -16,10 +16,14 @@
       
   100  
       
   101  
       
   102  import mock
       
   103 +from oslo.config import cfg
       
   104  
       
   105  from nova.console import websocketproxy
       
   106 +from nova import exception
       
   107  from nova import test
       
   108  
       
   109 +CONF = cfg.CONF
       
   110 +
       
   111  
       
   112  class NovaProxyRequestHandlerBaseTestCase(test.TestCase):
       
   113  
       
   114 @@ -31,15 +35,82 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase):
       
   115          self.wh.msg = mock.MagicMock()
       
   116          self.wh.do_proxy = mock.MagicMock()
       
   117          self.wh.headers = mock.MagicMock()
       
   118 +        CONF.set_override('novncproxy_base_url',
       
   119 +                          'https://example.net:6080/vnc_auto.html')
       
   120 +        CONF.set_override('html5proxy_base_url',
       
   121 +                          'https://example.net:6080/vnc_auto.html',
       
   122 +                          'spice')
       
   123 +
       
   124 +    def _fake_getheader(self, header):
       
   125 +        if header == 'cookie':
       
   126 +            return 'token="123-456-789"'
       
   127 +        elif header == 'Origin':
       
   128 +            return 'https://example.net:6080'
       
   129 +        elif header == 'Host':
       
   130 +            return 'example.net:6080'
       
   131 +        else:
       
   132 +            return
       
   133 +
       
   134 +    def _fake_getheader_bad_token(self, header):
       
   135 +        if header == 'cookie':
       
   136 +            return 'token="XXX"'
       
   137 +        elif header == 'Origin':
       
   138 +            return 'https://example.net:6080'
       
   139 +        elif header == 'Host':
       
   140 +            return 'example.net:6080'
       
   141 +        else:
       
   142 +            return
       
   143 +
       
   144 +    def _fake_getheader_bad_origin(self, header):
       
   145 +        if header == 'cookie':
       
   146 +            return 'token="123-456-789"'
       
   147 +        elif header == 'Origin':
       
   148 +            return 'https://bad-origin-example.net:6080'
       
   149 +        elif header == 'Host':
       
   150 +            return 'example.net:6080'
       
   151 +        else:
       
   152 +            return
       
   153 +
       
   154 +    def _fake_getheader_blank_origin(self, header):
       
   155 +        if header == 'cookie':
       
   156 +            return 'token="123-456-789"'
       
   157 +        elif header == 'Origin':
       
   158 +            return ''
       
   159 +        elif header == 'Host':
       
   160 +            return 'example.net:6080'
       
   161 +        else:
       
   162 +            return
       
   163 +
       
   164 +    def _fake_getheader_no_origin(self, header):
       
   165 +        if header == 'cookie':
       
   166 +            return 'token="123-456-789"'
       
   167 +        elif header == 'Origin':
       
   168 +            return None
       
   169 +        elif header == 'Host':
       
   170 +            return 'any-example.net:6080'
       
   171 +        else:
       
   172 +            return
       
   173 +
       
   174 +    def _fake_getheader_http(self, header):
       
   175 +        if header == 'cookie':
       
   176 +            return 'token="123-456-789"'
       
   177 +        elif header == 'Origin':
       
   178 +            return 'http://example.net:6080'
       
   179 +        elif header == 'Host':
       
   180 +            return 'example.net:6080'
       
   181 +        else:
       
   182 +            return
       
   183  
       
   184      @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
       
   185      def test_new_websocket_client(self, check_token):
       
   186          check_token.return_value = {
       
   187              'host': 'node1',
       
   188 -            'port': '10000'
       
   189 +            'port': '10000',
       
   190 +            'console_type': 'novnc'
       
   191          }
       
   192          self.wh.socket.return_value = '<socket>'
       
   193          self.wh.path = "ws://127.0.0.1/?token=123-456-789"
       
   194 +        self.wh.headers.getheader = self._fake_getheader
       
   195  
       
   196          self.wh.new_websocket_client()
       
   197  
       
   198 @@ -52,6 +123,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase):
       
   199          check_token.return_value = False
       
   200  
       
   201          self.wh.path = "ws://127.0.0.1/?token=XXX"
       
   202 +        self.wh.headers.getheader = self._fake_getheader
       
   203  
       
   204          self.assertRaises(Exception, self.wh.new_websocket_client)  # noqa
       
   205          check_token.assert_called_with(mock.ANY, token="XXX")
       
   206 @@ -60,11 +132,12 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase):
       
   207      def test_new_websocket_client_novnc(self, check_token):
       
   208          check_token.return_value = {
       
   209              'host': 'node1',
       
   210 -            'port': '10000'
       
   211 +            'port': '10000',
       
   212 +            'console_type': 'novnc'
       
   213          }
       
   214          self.wh.socket.return_value = '<socket>'
       
   215          self.wh.path = "http://127.0.0.1/"
       
   216 -        self.wh.headers.getheader.return_value = "token=123-456-789"
       
   217 +        self.wh.headers.getheader = self._fake_getheader
       
   218  
       
   219          self.wh.new_websocket_client()
       
   220  
       
   221 @@ -77,7 +150,111 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase):
       
   222          check_token.return_value = False
       
   223  
       
   224          self.wh.path = "http://127.0.0.1/"
       
   225 -        self.wh.headers.getheader.return_value = "token=XXX"
       
   226 +        self.wh.headers.getheader = self._fake_getheader_bad_token
       
   227  
       
   228          self.assertRaises(Exception, self.wh.new_websocket_client)  # noqa
       
   229          check_token.assert_called_with(mock.ANY, token="XXX")
       
   230 +
       
   231 +    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
       
   232 +    def test_new_websocket_client_novnc_bad_origin_header(self, check_token):
       
   233 +        check_token.return_value = {
       
   234 +            'host': 'node1',
       
   235 +            'port': '10000',
       
   236 +            'console_type': 'novnc'
       
   237 +        }
       
   238 +
       
   239 +        self.wh.path = "http://127.0.0.1/"
       
   240 +        self.wh.headers.getheader = self._fake_getheader_bad_origin
       
   241 +
       
   242 +        self.assertRaises(exception.ValidationError,
       
   243 +                          self.wh.new_websocket_client)
       
   244 +
       
   245 +    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
       
   246 +    def test_new_websocket_client_novnc_blank_origin_header(self, check_token):
       
   247 +        check_token.return_value = {
       
   248 +            'host': 'node1',
       
   249 +            'port': '10000',
       
   250 +            'console_type': 'novnc'
       
   251 +        }
       
   252 +
       
   253 +        self.wh.path = "http://127.0.0.1/"
       
   254 +        self.wh.headers.getheader = self._fake_getheader_blank_origin
       
   255 +
       
   256 +        self.assertRaises(exception.ValidationError,
       
   257 +                          self.wh.new_websocket_client)
       
   258 +
       
   259 +    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
       
   260 +    def test_new_websocket_client_novnc_no_origin_header(self, check_token):
       
   261 +        check_token.return_value = {
       
   262 +            'host': 'node1',
       
   263 +            'port': '10000',
       
   264 +            'console_type': 'novnc'
       
   265 +        }
       
   266 +        self.wh.socket.return_value = '<socket>'
       
   267 +        self.wh.path = "http://127.0.0.1/"
       
   268 +        self.wh.headers.getheader = self._fake_getheader_no_origin
       
   269 +
       
   270 +        self.wh.new_websocket_client()
       
   271 +
       
   272 +        check_token.assert_called_with(mock.ANY, token="123-456-789")
       
   273 +        self.wh.socket.assert_called_with('node1', 10000, connect=True)
       
   274 +        self.wh.do_proxy.assert_called_with('<socket>')
       
   275 +
       
   276 +    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
       
   277 +    def test_new_websocket_client_novnc_bad_origin_proto_vnc(self,
       
   278 +                                                             check_token):
       
   279 +        check_token.return_value = {
       
   280 +            'host': 'node1',
       
   281 +            'port': '10000',
       
   282 +            'console_type': 'novnc'
       
   283 +        }
       
   284 +
       
   285 +        self.wh.path = "http://127.0.0.1/"
       
   286 +        self.wh.headers.getheader = self._fake_getheader_http
       
   287 +
       
   288 +        self.assertRaises(exception.ValidationError,
       
   289 +                          self.wh.new_websocket_client)
       
   290 +
       
   291 +    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
       
   292 +    def test_new_websocket_client_novnc_bad_origin_proto_spice(self,
       
   293 +                                                               check_token):
       
   294 +        check_token.return_value = {
       
   295 +            'host': 'node1',
       
   296 +            'port': '10000',
       
   297 +            'console_type': 'spice-html5'
       
   298 +        }
       
   299 +
       
   300 +        self.wh.path = "http://127.0.0.1/"
       
   301 +        self.wh.headers.getheader = self._fake_getheader_http
       
   302 +
       
   303 +        self.assertRaises(exception.ValidationError,
       
   304 +                          self.wh.new_websocket_client)
       
   305 +
       
   306 +    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
       
   307 +    def test_new_websocket_client_novnc_bad_origin_proto_serial(self,
       
   308 +                                                                check_token):
       
   309 +        check_token.return_value = {
       
   310 +            'host': 'node1',
       
   311 +            'port': '10000',
       
   312 +            'console_type': 'serial'
       
   313 +        }
       
   314 +
       
   315 +        self.wh.path = "http://127.0.0.1/"
       
   316 +        self.wh.headers.getheader = self._fake_getheader_http
       
   317 +
       
   318 +        self.assertRaises(exception.ValidationError,
       
   319 +                          self.wh.new_websocket_client)
       
   320 +
       
   321 +    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
       
   322 +    def test_new_websocket_client_novnc_bad_console_type(self, check_token):
       
   323 +        check_token.return_value = {
       
   324 +            'host': 'node1',
       
   325 +            'port': '10000',
       
   326 +            'console_type': 'bad-console-type'
       
   327 +        }
       
   328 +
       
   329 +        self.wh.path = "http://127.0.0.1/"
       
   330 +        self.wh.headers.getheader = self._fake_getheader
       
   331 +
       
   332 +        self.assertRaises(exception.ValidationError,
       
   333 +                          self.wh.new_websocket_client)
       
   334 -- 
       
   335 1.7.9.5
       
   336