From 972986f4697c29eb8b58964994f8db50774a1e87 Mon Sep 17 00:00:00 2001 From: Dave McCowan Date: Mon, 26 Jan 2015 00:26:31 -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 script attack. This commit adds a check to verify the origin matches the host. Closes-Bug: 1409142 --- nova/console/websocketproxy.py | 9 +++++ nova/tests/unit/console/test_websocketproxy.py | 50 ++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/nova/console/websocketproxy.py b/nova/console/websocketproxy.py index 4d32ff4..848df7c 100644 --- a/nova/console/websocketproxy.py +++ b/nova/console/websocketproxy.py @@ -53,6 +53,15 @@ class NovaProxyRequestHandlerBase(object): _("We do not support scheme '%s' under Python < 2.7.4, " "please use http or https") % parse.scheme) + # Verify Origin header to prevent XSS attack + verify_host = self.headers.getheader('Host') + origin_url = self.headers.getheader('Origin') + origin_parsed = urlparse.urlparse(origin_url) + verify_origin = origin_parsed.netloc + if verify_host != verify_origin: + detail = _("Origin header does not match this host.") + raise exception.ValidationError(detail=detail) + query = parse.query token = urlparse.parse_qs(query).get("token", [""]).pop() if not token: diff --git a/nova/tests/unit/console/test_websocketproxy.py b/nova/tests/unit/console/test_websocketproxy.py index c0526a2..6e611bb 100644 --- a/nova/tests/unit/console/test_websocketproxy.py +++ b/nova/tests/unit/console/test_websocketproxy.py @@ -33,6 +33,36 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase): self.wh.do_proxy = mock.MagicMock() self.wh.headers = mock.MagicMock() + 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 + @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token') def test_new_websocket_client(self, check_token): check_token.return_value = { @@ -41,6 +71,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase): } self.wh.socket.return_value = '' self.wh.path = "http://127.0.0.1/?token=123-456-789" + self.wh.headers.getheader = self._fake_getheader self.wh.new_websocket_client() @@ -53,6 +84,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase): check_token.return_value = False self.wh.path = "http://127.0.0.1/?token=XXX" + self.wh.headers.getheader = self._fake_getheader self.assertRaises(exception.InvalidToken, self.wh.new_websocket_client) @@ -66,7 +98,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase): } self.wh.socket.return_value = '' 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() @@ -79,7 +111,7 @@ 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.InvalidToken, self.wh.new_websocket_client) @@ -98,6 +130,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase): self.wh.socket.return_value = tsock self.wh.path = "http://127.0.0.1/?token=123-456-789" + self.wh.headers.getheader = self._fake_getheader self.wh.new_websocket_client() @@ -118,6 +151,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase): self.wh.socket.return_value = tsock self.wh.path = "http://127.0.0.1/?token=123-456-789" + self.wh.headers.getheader = self._fake_getheader self.assertRaises(exception.InvalidConnectionInfo, self.wh.new_websocket_client) @@ -134,6 +168,7 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase): } self.wh.socket.return_value = '' self.wh.path = "http://127.0.0.1/?token=123-456-789" + self.wh.headers.getheader = self._fake_getheader self.wh.new_websocket_client() @@ -152,6 +187,17 @@ class NovaProxyRequestHandlerBaseTestCase(test.TestCase): } self.wh.socket.return_value = '' self.wh.path = "ws://127.0.0.1/?token=123-456-789" + self.wh.headers.getheader = self._fake_getheader self.assertRaises(exception.NovaException, self.wh.new_websocket_client) + + @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token') + def test_new_websocket_client_novnc_header_invalid(self, check_token): + check_token.return_value = False + + 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) -- 1.9.1