CVE-2015-9543
Description
An issue was discovered in OpenStack Nova before 18.2.4, 19.x before 19.1.0, and 20.x before 20.1.0. It can leak consoleauth tokens into log files. An attacker with read access to the service's logs may obtain tokens used for console access. All Nova setups using novncproxy are affected. This is related to NovaProxyRequestHandlerBase.new_websocket_client in console/websocketproxy.py.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
OpenStack Nova consoles proxy leaks consoleauth tokens into logs, enabling attackers with log read access to obtain VNC console tokens.
Vulnerability
An issue in OpenStack Nova's console proxy (novncproxy) causes console authentication tokens to be written into log files at INFO level [1]. The flaw resides in NovaProxyRequestHandlerBase.new_websocket_client in console/websocketproxy.py.
Exploitation
An attacker with read access to the service's logs can extract these tokens [2]. The tokens are logged both by nova-consoleauth and nova-novncproxy [3]. No additional authentication is required beyond log access.
Impact
With a valid token, an attacker can access the corresponding instance's VNC console, potentially leading to data exposure or denial of service via console actions (e.g., Ctrl+Alt+Del) [3].
Mitigation
The issue is fixed in Nova versions 18.2.4, 19.1.0, and 20.1.0 [1]. Patches mask the token in log messages [4]. Users should upgrade or apply the relevant patches.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
NovaPyPI | < 18.2.4 | 18.2.4 |
NovaPyPI | >= 19.0.0, < 19.1.0 | 19.1.0 |
NovaPyPI | >= 20.0.0, < 20.1.0 | 20.1.0 |
Affected products
2- OpenStack/Novadescription
Patches
308f1f914cc21Mask the token used to allow access to consoles
4 files changed · +32 −7
nova/consoleauth/manager.py+4 −5 modified@@ -100,9 +100,8 @@ def authorize_console(self, context, token, console_type, host, port, self.mc_instance.set(instance_uuid.encode('UTF-8'), jsonutils.dumps(tokens)) - - LOG.info("Received Token: %(token)s, %(token_dict)s", - {'token': token, 'token_dict': token_dict}) + token_dict['token'] = '***' + LOG.info("Received Token: %(token_dict)s", {'token_dict': token_dict}) def _validate_token(self, context, token): instance_uuid = token['instance_uuid'] @@ -130,8 +129,8 @@ def _validate_token(self, context, token): def check_token(self, context, token): token_str = self.mc.get(token.encode('UTF-8')) token_valid = (token_str is not None) - LOG.info("Checking Token: %(token)s, %(token_valid)s", - {'token': token, 'token_valid': token_valid}) + LOG.info("Checking that token is known: %(token_valid)s", + {'token_valid': token_valid}) if token_valid: token = jsonutils.loads(token_str) if self._validate_token(context, token):
nova/console/websocketproxy.py+5 −1 modified@@ -18,6 +18,7 @@ Leverages websockify.py by Joel Martin ''' +import copy import socket import sys @@ -248,7 +249,10 @@ def new_websocket_client(self): detail = _("Origin header protocol does not match this host.") raise exception.ValidationError(detail=detail) - self.msg(_('connect info: %s'), str(connect_info)) + sanitized_info = copy.copy(connect_info) + sanitized_info['token'] = '***' + self.msg(_('connect info: %s'), sanitized_info) + host = connect_info['host'] port = int(connect_info['port'])
nova/tests/unit/consoleauth/test_consoleauth.py+20 −1 modified@@ -88,6 +88,17 @@ def fake_validate_console_port(self, ctxt, instance, self.stub_out(self.rpcapi + 'validate_console_port', fake_validate_console_port) + @mock.patch('nova.consoleauth.manager.LOG.info') + def test_authorize_does_not_log_token_secrete(self, mock_info): + self.manager_api.authorize_console( + self.context, 'secret', 'novnc', '127.0.0.1', '8080', 'host', + self.instance_uuid) + + mock_info.assert_called_once_with( + 'Received Token: %(token_dict)s', test.MatchType(dict)) + self.assertEqual( + '***', mock_info.mock_calls[0][1][1]['token_dict']['token']) + @mock.patch('nova.objects.instance.Instance.get_by_uuid') def test_multiple_tokens_for_instance(self, mock_get): mock_get.return_value = None @@ -139,8 +150,9 @@ def test_delete_tokens_for_instance_no_tokens(self): mock_delete.assert_called_once_with( self.instance_uuid.encode('UTF-8')) + @mock.patch('nova.consoleauth.manager.LOG.info') @mock.patch('nova.objects.instance.Instance.get_by_uuid') - def test_wrong_token_has_port(self, mock_get): + def test_wrong_token_has_port(self, mock_get, mock_log): mock_get.return_value = None token = u'mytok' @@ -151,6 +163,13 @@ def test_wrong_token_has_port(self, mock_get): '127.0.0.1', '8080', 'host', instance_uuid=self.instance_uuid) self.assertIsNone(self.manager_api.check_token(self.context, token)) + mock_log.assert_has_calls([ + mock.call( + 'Received Token: %(token_dict)s', mock.ANY), + mock.call( + 'Checking that token is known: %(token_valid)s', + {'token_valid': True}), + ]) def test_delete_expired_tokens(self): self.useFixture(test.TimeOverride())
nova/tests/unit/console/test_websocketproxy.py+3 −0 modified@@ -295,6 +295,9 @@ def test_new_websocket_client(self, validate, check_port): validate.assert_called_with(mock.ANY, "123-456-789") self.wh.socket.assert_called_with('node1', 10000, connect=True) self.wh.do_proxy.assert_called_with('<socket>') + # ensure that token is masked when logged + connection_info = self.wh.msg.mock_calls[0][1][1] + self.assertEqual('***', connection_info['token']) @mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.' '_check_console_port')
26d4047e17ebMask the token used to allow access to consoles
2 files changed · +8 −1
nova/console/websocketproxy.py+5 −1 modified@@ -18,6 +18,7 @@ Leverages websockify.py by Joel Martin ''' +import copy import socket import sys @@ -220,7 +221,10 @@ def new_websocket_client(self): detail = _("Origin header protocol does not match this host.") raise exception.ValidationError(detail=detail) - self.msg(_('connect info: %s'), str(connect_info)) + sanitized_info = copy.copy(connect_info) + sanitized_info.token = '***' + self.msg(_('connect info: %s'), sanitized_info) + host = connect_info.host port = connect_info.port
nova/tests/unit/console/test_websocketproxy.py+3 −0 modified@@ -219,6 +219,9 @@ def test_new_websocket_client(self, validate, check_port): validate.assert_called_with(mock.ANY, "123-456-789") self.wh.socket.assert_called_with('node1', 10000, connect=True) self.wh.do_proxy.assert_called_with('<socket>') + # ensure that token is masked when logged + connection_info = self.wh.msg.mock_calls[0][1][1] + self.assertEqual('***', connection_info.token) @mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.' '_check_console_port')
d8fbf04f325fMask the token used to allow access to consoles
4 files changed · +32 −7
nova/consoleauth/manager.py+4 −5 modified@@ -100,9 +100,8 @@ def authorize_console(self, context, token, console_type, host, port, self.mc_instance.set(instance_uuid.encode('UTF-8'), jsonutils.dumps(tokens)) - - LOG.info("Received Token: %(token)s, %(token_dict)s", - {'token': token, 'token_dict': token_dict}) + token_dict['token'] = '***' + LOG.info("Received Token: %(token_dict)s", {'token_dict': token_dict}) def _validate_token(self, context, token): instance_uuid = token['instance_uuid'] @@ -130,8 +129,8 @@ def _validate_token(self, context, token): def check_token(self, context, token): token_str = self.mc.get(token.encode('UTF-8')) token_valid = (token_str is not None) - LOG.info("Checking Token: %(token)s, %(token_valid)s", - {'token': token, 'token_valid': token_valid}) + LOG.info("Checking that token is known: %(token_valid)s", + {'token_valid': token_valid}) if token_valid: token = jsonutils.loads(token_str) if self._validate_token(context, token):
nova/console/websocketproxy.py+5 −1 modified@@ -18,6 +18,7 @@ Leverages websockify.py by Joel Martin ''' +import copy import socket import sys @@ -248,7 +249,10 @@ def new_websocket_client(self): detail = _("Origin header protocol does not match this host.") raise exception.ValidationError(detail=detail) - self.msg(_('connect info: %s'), str(connect_info)) + sanitized_info = copy.copy(connect_info) + sanitized_info['token'] = '***' + self.msg(_('connect info: %s'), sanitized_info) + host = connect_info['host'] port = int(connect_info['port'])
nova/tests/unit/consoleauth/test_consoleauth.py+20 −1 modified@@ -88,6 +88,17 @@ def fake_validate_console_port(self, ctxt, instance, self.stub_out(self.rpcapi + 'validate_console_port', fake_validate_console_port) + @mock.patch('nova.consoleauth.manager.LOG.info') + def test_authorize_does_not_log_token_secrete(self, mock_info): + self.manager_api.authorize_console( + self.context, 'secret', 'novnc', '127.0.0.1', '8080', 'host', + self.instance_uuid) + + mock_info.assert_called_once_with( + 'Received Token: %(token_dict)s', test.MatchType(dict)) + self.assertEqual( + '***', mock_info.mock_calls[0][1][1]['token_dict']['token']) + @mock.patch('nova.objects.instance.Instance.get_by_uuid') def test_multiple_tokens_for_instance(self, mock_get): mock_get.return_value = None @@ -139,8 +150,9 @@ def test_delete_tokens_for_instance_no_tokens(self): mock_delete.assert_called_once_with( self.instance_uuid.encode('UTF-8')) + @mock.patch('nova.consoleauth.manager.LOG.info') @mock.patch('nova.objects.instance.Instance.get_by_uuid') - def test_wrong_token_has_port(self, mock_get): + def test_wrong_token_has_port(self, mock_get, mock_log): mock_get.return_value = None token = u'mytok' @@ -151,6 +163,13 @@ def test_wrong_token_has_port(self, mock_get): '127.0.0.1', '8080', 'host', instance_uuid=self.instance_uuid) self.assertIsNone(self.manager_api.check_token(self.context, token)) + mock_log.assert_has_calls([ + mock.call( + 'Received Token: %(token_dict)s', mock.ANY), + mock.call( + 'Checking that token is known: %(token_valid)s', + {'token_valid': True}), + ]) def test_delete_expired_tokens(self): self.useFixture(test.TimeOverride())
nova/tests/unit/console/test_websocketproxy.py+3 −0 modified@@ -295,6 +295,9 @@ def test_new_websocket_client(self, validate, check_port): validate.assert_called_with(mock.ANY, "123-456-789") self.wh.socket.assert_called_with('node1', 10000, connect=True) self.wh.do_proxy.assert_called_with('<socket>') + # ensure that token is masked when logged + connection_info = self.wh.msg.mock_calls[0][1][1] + self.assertEqual('***', connection_info['token']) @mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.' '_check_console_port')
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-22jm-4hxw-35jfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2015-9543ghsaADVISORY
- www.openwall.com/lists/oss-security/2020/02/19/2ghsamailing-listx_refsource_MLISTWEB
- github.com/openstack/nova/commit/08f1f914cc219cf526adfb08c46b8f40b4e78232ghsaWEB
- github.com/openstack/nova/commit/26d4047e17eba9bc271f8868f1d0ffeec97b555eghsaWEB
- github.com/openstack/nova/commit/d8fbf04f325f593836f8d44b6bbf42b85bde94e3ghsaWEB
- launchpad.net/bugs/1492140ghsax_refsource_MISCWEB
- review.opendev.org/220622ghsax_refsource_MISCWEB
- security.openstack.org/ossa/OSSA-2020-001.htmlghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.