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 |
|