components/openstack/nova/patches/07-CVE-2015-0259.patch
changeset 3998 5bd484384122
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/openstack/nova/patches/07-CVE-2015-0259.patch	Thu Mar 19 14:41:20 2015 -0700
@@ -0,0 +1,336 @@
+Upstream patch to address CVE-2015-0259.  This fix will be included in
+the future 2014.2.3 (juno) release.
+
+From 676ba7bbc788a528b0fe4c87c1c4bf94b4bb6eb1 Mon Sep 17 00:00:00 2001
+From: Dave McCowan <[email protected]>
+Date: Tue, 24 Feb 2015 21:35:48 -0500
+Subject: [PATCH] Websocket Proxy should verify Origin header
+
+If the Origin HTTP header passed in the WebSocket handshake does
+not match the host, this could indicate an attempt at a
+cross-site attack.  This commit adds a check to verify
+the origin matches the host.
+
+Change-Id: Ica6ec23d6f69a236657d5ba0c3f51b693c633649
+Closes-Bug: 1409142
+---
+ nova/console/websocketproxy.py            |   45 +++++++
+ nova/tests/console/test_websocketproxy.py |  185 ++++++++++++++++++++++++++++-
+ 2 files changed, 226 insertions(+), 4 deletions(-)
+
+diff --git a/nova/console/websocketproxy.py b/nova/console/websocketproxy.py
+index ef684f5..7a1e056 100644
+--- a/nova/console/websocketproxy.py
++++ b/nova/console/websocketproxy.py
+@@ -22,17 +22,40 @@ import Cookie
+ import socket
+ import urlparse
+ 
++from oslo.config import cfg
+ import websockify
+ 
+ from nova.consoleauth import rpcapi as consoleauth_rpcapi
+ from nova import context
++from nova import exception
+ from nova.i18n import _
+ from nova.openstack.common import log as logging
+ 
+ LOG = logging.getLogger(__name__)
+ 
++CONF = cfg.CONF
++CONF.import_opt('novncproxy_base_url', 'nova.vnc')
++CONF.import_opt('html5proxy_base_url', 'nova.spice', group='spice')
++CONF.import_opt('base_url', 'nova.console.serial', group='serial_console')
++
+ 
+ class NovaProxyRequestHandlerBase(object):
++    def verify_origin_proto(self, console_type, origin_proto):
++        if console_type == 'novnc':
++            expected_proto = \
++                urlparse.urlparse(CONF.novncproxy_base_url).scheme
++        elif console_type == 'spice-html5':
++            expected_proto = \
++                urlparse.urlparse(CONF.spice.html5proxy_base_url).scheme
++        elif console_type == 'serial':
++            expected_proto = \
++                urlparse.urlparse(CONF.serial_console.base_url).scheme
++        else:
++            detail = _("Invalid Console Type for WebSocketProxy: '%s'") % \
++                        console_type
++            raise exception.ValidationError(detail=detail)
++        return origin_proto == expected_proto
++
+     def new_websocket_client(self):
+         """Called after a new WebSocket connection has been established."""
+         # Reopen the eventlet hub to make sure we don't share an epoll
+@@ -62,6 +85,28 @@ class NovaProxyRequestHandlerBase(object):
+         if not connect_info:
+             raise Exception(_("Invalid Token"))
+ 
++        # Verify Origin
++        expected_origin_hostname = self.headers.getheader('Host')
++        if ':' in expected_origin_hostname:
++            e = expected_origin_hostname
++            expected_origin_hostname = e.split(':')[0]
++        origin_url = self.headers.getheader('Origin')
++        # missing origin header indicates non-browser client which is OK
++        if origin_url is not None:
++            origin = urlparse.urlparse(origin_url)
++            origin_hostname = origin.hostname
++            origin_scheme = origin.scheme
++            if origin_hostname == '' or origin_scheme == '':
++                detail = _("Origin header not valid.")
++                raise exception.ValidationError(detail=detail)
++            if expected_origin_hostname != origin_hostname:
++                detail = _("Origin header does not match this host.")
++                raise exception.ValidationError(detail=detail)
++            if not self.verify_origin_proto(connect_info['console_type'],
++                                              origin.scheme):
++                detail = _("Origin header protocol does not match this host.")
++                raise exception.ValidationError(detail=detail)
++
+         self.msg(_('connect info: %s'), str(connect_info))
+         host = connect_info['host']
+         port = int(connect_info['port'])
+diff --git a/nova/tests/console/test_websocketproxy.py b/nova/tests/console/test_websocketproxy.py
+index 1e51a4d..66913c2 100644
+--- a/nova/tests/console/test_websocketproxy.py
++++ b/nova/tests/console/test_websocketproxy.py
+@@ -16,10 +16,14 @@
+ 
+ 
+ import mock
++from oslo.config import cfg
+ 
+ from nova.console import websocketproxy
++from nova import exception
+ from nova import test
+ 
++CONF = cfg.CONF
++
+ 
+ class NovaProxyRequestHandlerBaseTestCase(test.TestCase):
+ 
+@@ -31,15 +35,82 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase):
+         self.wh.msg = mock.MagicMock()
+         self.wh.do_proxy = mock.MagicMock()
+         self.wh.headers = mock.MagicMock()
++        CONF.set_override('novncproxy_base_url',
++                          'https://example.net:6080/vnc_auto.html')
++        CONF.set_override('html5proxy_base_url',
++                          'https://example.net:6080/vnc_auto.html',
++                          'spice')
++
++    def _fake_getheader(self, header):
++        if header == 'cookie':
++            return 'token="123-456-789"'
++        elif header == 'Origin':
++            return 'https://example.net:6080'
++        elif header == 'Host':
++            return 'example.net:6080'
++        else:
++            return
++
++    def _fake_getheader_bad_token(self, header):
++        if header == 'cookie':
++            return 'token="XXX"'
++        elif header == 'Origin':
++            return 'https://example.net:6080'
++        elif header == 'Host':
++            return 'example.net:6080'
++        else:
++            return
++
++    def _fake_getheader_bad_origin(self, header):
++        if header == 'cookie':
++            return 'token="123-456-789"'
++        elif header == 'Origin':
++            return 'https://bad-origin-example.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"'
++        elif header == 'Origin':
++            return ''
++        elif header == 'Host':
++            return 'example.net:6080'
++        else:
++            return
++
++    def _fake_getheader_no_origin(self, header):
++        if header == 'cookie':
++            return 'token="123-456-789"'
++        elif header == 'Origin':
++            return None
++        elif header == 'Host':
++            return 'any-example.net:6080'
++        else:
++            return
++
++    def _fake_getheader_http(self, header):
++        if header == 'cookie':
++            return 'token="123-456-789"'
++        elif header == 'Origin':
++            return 'http://example.net:6080'
++        elif header == 'Host':
++            return 'example.net:6080'
++        else:
++            return
+ 
+     @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
+     def test_new_websocket_client(self, check_token):
+         check_token.return_value = {
+             'host': 'node1',
+-            'port': '10000'
++            'port': '10000',
++            'console_type': 'novnc'
+         }
+         self.wh.socket.return_value = '<socket>'
+         self.wh.path = "ws://127.0.0.1/?token=123-456-789"
++        self.wh.headers.getheader = self._fake_getheader
+ 
+         self.wh.new_websocket_client()
+ 
+@@ -52,6 +123,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase):
+         check_token.return_value = False
+ 
+         self.wh.path = "ws://127.0.0.1/?token=XXX"
++        self.wh.headers.getheader = self._fake_getheader
+ 
+         self.assertRaises(Exception, self.wh.new_websocket_client)  # noqa
+         check_token.assert_called_with(mock.ANY, token="XXX")
+@@ -60,11 +132,12 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase):
+     def test_new_websocket_client_novnc(self, check_token):
+         check_token.return_value = {
+             'host': 'node1',
+-            'port': '10000'
++            'port': '10000',
++            'console_type': 'novnc'
+         }
+         self.wh.socket.return_value = '<socket>'
+         self.wh.path = "http://127.0.0.1/"
+-        self.wh.headers.getheader.return_value = "token=123-456-789"
++        self.wh.headers.getheader = self._fake_getheader
+ 
+         self.wh.new_websocket_client()
+ 
+@@ -77,7 +150,111 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase):
+         check_token.return_value = False
+ 
+         self.wh.path = "http://127.0.0.1/"
+-        self.wh.headers.getheader.return_value = "token=XXX"
++        self.wh.headers.getheader = self._fake_getheader_bad_token
+ 
+         self.assertRaises(Exception, self.wh.new_websocket_client)  # noqa
+         check_token.assert_called_with(mock.ANY, token="XXX")
++
++    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
++    def test_new_websocket_client_novnc_bad_origin_header(self, check_token):
++        check_token.return_value = {
++            'host': 'node1',
++            'port': '10000',
++            'console_type': 'novnc'
++        }
++
++        self.wh.path = "http://127.0.0.1/"
++        self.wh.headers.getheader = self._fake_getheader_bad_origin
++
++        self.assertRaises(exception.ValidationError,
++                          self.wh.new_websocket_client)
++
++    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
++    def test_new_websocket_client_novnc_blank_origin_header(self, check_token):
++        check_token.return_value = {
++            'host': 'node1',
++            'port': '10000',
++            'console_type': 'novnc'
++        }
++
++        self.wh.path = "http://127.0.0.1/"
++        self.wh.headers.getheader = self._fake_getheader_blank_origin
++
++        self.assertRaises(exception.ValidationError,
++                          self.wh.new_websocket_client)
++
++    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
++    def test_new_websocket_client_novnc_no_origin_header(self, check_token):
++        check_token.return_value = {
++            'host': 'node1',
++            'port': '10000',
++            'console_type': 'novnc'
++        }
++        self.wh.socket.return_value = '<socket>'
++        self.wh.path = "http://127.0.0.1/"
++        self.wh.headers.getheader = self._fake_getheader_no_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('<socket>')
++
++    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
++    def test_new_websocket_client_novnc_bad_origin_proto_vnc(self,
++                                                             check_token):
++        check_token.return_value = {
++            'host': 'node1',
++            'port': '10000',
++            'console_type': 'novnc'
++        }
++
++        self.wh.path = "http://127.0.0.1/"
++        self.wh.headers.getheader = self._fake_getheader_http
++
++        self.assertRaises(exception.ValidationError,
++                          self.wh.new_websocket_client)
++
++    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
++    def test_new_websocket_client_novnc_bad_origin_proto_spice(self,
++                                                               check_token):
++        check_token.return_value = {
++            'host': 'node1',
++            'port': '10000',
++            'console_type': 'spice-html5'
++        }
++
++        self.wh.path = "http://127.0.0.1/"
++        self.wh.headers.getheader = self._fake_getheader_http
++
++        self.assertRaises(exception.ValidationError,
++                          self.wh.new_websocket_client)
++
++    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
++    def test_new_websocket_client_novnc_bad_origin_proto_serial(self,
++                                                                check_token):
++        check_token.return_value = {
++            'host': 'node1',
++            'port': '10000',
++            'console_type': 'serial'
++        }
++
++        self.wh.path = "http://127.0.0.1/"
++        self.wh.headers.getheader = self._fake_getheader_http
++
++        self.assertRaises(exception.ValidationError,
++                          self.wh.new_websocket_client)
++
++    @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
++    def test_new_websocket_client_novnc_bad_console_type(self, check_token):
++        check_token.return_value = {
++            'host': 'node1',
++            'port': '10000',
++            'console_type': 'bad-console-type'
++        }
++
++        self.wh.path = "http://127.0.0.1/"
++        self.wh.headers.getheader = self._fake_getheader
++
++        self.assertRaises(exception.ValidationError,
++                          self.wh.new_websocket_client)
+-- 
+1.7.9.5
+