High severity7.5NVD Advisory· Published Jul 12, 2016· Updated May 6, 2026
CVE-2016-4985
CVE-2016-4985
Description
The ironic-api service in OpenStack Ironic before 4.2.5 (Liberty) and 5.x before 5.1.2 (Mitaka) allows remote attackers to obtain sensitive information about a registered node by leveraging knowledge of the MAC address of a network card belonging to that node and sending a crafted POST request to the v1/drivers/$DRIVER_NAME/vendor_passthru resource.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ironicPyPI | < 4.2.5 | 4.2.5 |
ironicPyPI | >= 5.0, < 5.1.2 | 5.1.2 |
Affected products
5cpe:2.3:a:canonical:openstack_ironic:*:*:*:*:*:*:*:*+ 2 more
- cpe:2.3:a:canonical:openstack_ironic:*:*:*:*:*:*:*:*range: <=4.2.4
- cpe:2.3:a:canonical:openstack_ironic:5.1.0:*:*:*:*:*:*:*
- cpe:2.3:a:canonical:openstack_ironic:5.1.1:*:*:*:*:*:*:*
Patches
3426a306fb580Mask password on agent lookup according to policy
4 files changed · +38 −6
ironic/drivers/modules/agent_base_vendor.py+7 −1 modified@@ -16,6 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. +import ast import collections import time @@ -582,9 +583,14 @@ def lookup(self, context, **kwargs): LOG.info(_LI('Initial lookup for node %s succeeded, agent is running ' 'and waiting for commands'), node.uuid) + ndict = node.as_dict() + if not context.show_password: + ndict['driver_info'] = ast.literal_eval( + strutils.mask_password(ndict['driver_info'], "******")) + return { 'heartbeat_timeout': CONF.agent.heartbeat_timeout, - 'node': node.as_dict() + 'node': ndict, } def _get_completed_cleaning_command(self, task, commands):
ironic/tests/unit/db/utils.py+1 −0 modified@@ -152,6 +152,7 @@ def get_test_agent_driver_info(): return { 'deploy_kernel': 'glance://deploy_kernel_uuid', 'deploy_ramdisk': 'glance://deploy_ramdisk_uuid', + 'ipmi_password': 'foo', }
ironic/tests/unit/drivers/modules/test_agent_base_vendor.py+19 −5 modified@@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import time import types @@ -91,7 +92,8 @@ def test_lookup_version_not_found(self): @mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor' '._find_node_by_macs', autospec=True) - def test_lookup_v2(self, find_mock): + def _test_lookup_v2(self, find_mock, show_password=True): + self.context.show_password = show_password kwargs = { 'version': '2', 'inventory': { @@ -108,10 +110,20 @@ def test_lookup_v2(self, find_mock): ] } } + # NOTE(jroll) apparently as_dict() returns a dict full of references + expected = copy.deepcopy(self.node.as_dict()) + if not show_password: + expected['driver_info']['ipmi_password'] = '******' find_mock.return_value = self.node with task_manager.acquire(self.context, self.node.uuid) as task: node = self.passthru.lookup(task.context, **kwargs) - self.assertEqual(self.node.as_dict(), node['node']) + self.assertEqual(expected, node['node']) + + def test_lookup_v2_show_password(self): + self._test_lookup_v2(show_password=True) + + def test_lookup_v2_hide_password(self): + self._test_lookup_v2(show_password=False) def test_lookup_v2_missing_inventory(self): with task_manager.acquire(self.context, self.node.uuid) as task: @@ -136,9 +148,11 @@ def test_lookup_v2_empty_interfaces(self): @mock.patch.object(objects.Node, 'get_by_uuid') def test_lookup_v2_with_node_uuid(self, mock_get_node): + self.context.show_password = True + expected = copy.deepcopy(self.node.as_dict()) kwargs = { 'version': '2', - 'node_uuid': 'fake uuid', + 'node_uuid': 'fake-uuid', 'inventory': { 'interfaces': [ { @@ -156,8 +170,8 @@ def test_lookup_v2_with_node_uuid(self, mock_get_node): mock_get_node.return_value = self.node with task_manager.acquire(self.context, self.node.uuid) as task: node = self.passthru.lookup(task.context, **kwargs) - self.assertEqual(self.node.as_dict(), node['node']) - mock_get_node.assert_called_once_with(mock.ANY, 'fake uuid') + self.assertEqual(expected, node['node']) + mock_get_node.assert_called_once_with(mock.ANY, 'fake-uuid') @mock.patch.object(objects.port.Port, 'get_by_address', spec_set=types.FunctionType)
releasenotes/notes/fix-cve-2016-4985-b62abae577025365.yaml+11 −0 added@@ -0,0 +1,11 @@ +--- +security: + - A critical security vulnerability (CVE-2016-4985) was fixed in this + release. Previously, a client with network access to the ironic-api service + was able to bypass Keystone authentication and retrieve all information + about any Node registered with Ironic, if they knew (or were able to guess) + the MAC address of a network card belonging to that Node, by sending a + crafted POST request to the /v1/drivers/$DRIVER_NAME/vendor_passthru + resource. Ironic's policy.json configuration is now respected when + responding to this request such that, if passwords should be masked for + other requests, they are also masked for this request.
affec2249771Mask password on agent lookup according to policy
4 files changed · +38 −6
ironic/drivers/modules/agent_base_vendor.py+7 −1 modified@@ -16,6 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. +import ast import collections import time @@ -535,9 +536,14 @@ def lookup(self, context, **kwargs): LOG.info(_LI('Initial lookup for node %s succeeded, agent is running ' 'and waiting for commands'), node.uuid) + ndict = node.as_dict() + if not context.show_password: + ndict['driver_info'] = ast.literal_eval( + strutils.mask_password(ndict['driver_info'], "******")) + return { 'heartbeat_timeout': CONF.agent.heartbeat_timeout, - 'node': node.as_dict() + 'node': ndict, } def _get_completed_cleaning_command(self, task):
ironic/tests/unit/db/utils.py+1 −0 modified@@ -152,6 +152,7 @@ def get_test_agent_driver_info(): return { 'deploy_kernel': 'glance://deploy_kernel_uuid', 'deploy_ramdisk': 'glance://deploy_ramdisk_uuid', + 'ipmi_password': 'foo', }
ironic/tests/unit/drivers/modules/test_agent_base_vendor.py+19 −5 modified@@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import time import types @@ -91,7 +92,8 @@ def test_lookup_version_not_found(self): @mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor' '._find_node_by_macs', autospec=True) - def test_lookup_v2(self, find_mock): + def _test_lookup_v2(self, find_mock, show_password=True): + self.context.show_password = show_password kwargs = { 'version': '2', 'inventory': { @@ -108,10 +110,20 @@ def test_lookup_v2(self, find_mock): ] } } + # NOTE(jroll) apparently as_dict() returns a dict full of references + expected = copy.deepcopy(self.node.as_dict()) + if not show_password: + expected['driver_info']['ipmi_password'] = '******' find_mock.return_value = self.node with task_manager.acquire(self.context, self.node.uuid) as task: node = self.passthru.lookup(task.context, **kwargs) - self.assertEqual(self.node.as_dict(), node['node']) + self.assertEqual(expected, node['node']) + + def test_lookup_v2_show_password(self): + self._test_lookup_v2(show_password=True) + + def test_lookup_v2_hide_password(self): + self._test_lookup_v2(show_password=False) def test_lookup_v2_missing_inventory(self): with task_manager.acquire(self.context, self.node.uuid) as task: @@ -136,9 +148,11 @@ def test_lookup_v2_empty_interfaces(self): @mock.patch.object(objects.Node, 'get_by_uuid') def test_lookup_v2_with_node_uuid(self, mock_get_node): + self.context.show_password = True + expected = copy.deepcopy(self.node.as_dict()) kwargs = { 'version': '2', - 'node_uuid': 'fake uuid', + 'node_uuid': 'fake-uuid', 'inventory': { 'interfaces': [ { @@ -156,8 +170,8 @@ def test_lookup_v2_with_node_uuid(self, mock_get_node): mock_get_node.return_value = self.node with task_manager.acquire(self.context, self.node.uuid) as task: node = self.passthru.lookup(task.context, **kwargs) - self.assertEqual(self.node.as_dict(), node['node']) - mock_get_node.assert_called_once_with(mock.ANY, 'fake uuid') + self.assertEqual(expected, node['node']) + mock_get_node.assert_called_once_with(mock.ANY, 'fake-uuid') @mock.patch.object(objects.port.Port, 'get_by_address', spec_set=types.FunctionType)
releasenotes/notes/fix-cve-2016-4985-b62abae577025365.yaml+11 −0 added@@ -0,0 +1,11 @@ +--- +security: + - A critical security vulnerability (CVE-2016-4985) was fixed in this + release. Previously, a client with network access to the ironic-api service + was able to bypass Keystone authentication and retrieve all information + about any Node registered with Ironic, if they knew (or were able to guess) + the MAC address of a network card belonging to that Node, by sending a + crafted POST request to the /v1/drivers/$DRIVER_NAME/vendor_passthru + resource. Ironic's policy.json configuration is now respected when + responding to this request such that, if passwords should be masked for + other requests, they are also masked for this request.
f5a3ff1dfcdeMask password on agent lookup according to policy
4 files changed · +39 −7
ironic/drivers/modules/agent_base_vendor.py+8 −2 modified@@ -16,12 +16,13 @@ # License for the specific language governing permissions and limitations # under the License. - +import ast import time from oslo_config import cfg from oslo_log import log from oslo_utils import excutils +from oslo_utils import strutils import retrying from ironic.common import boot_devices @@ -436,9 +437,14 @@ def lookup(self, context, **kwargs): LOG.info(_LI('Initial lookup for node %s succeeded, agent is running ' 'and waiting for commands'), node.uuid) + ndict = node.as_dict() + if not context.show_password: + ndict['driver_info'] = ast.literal_eval( + strutils.mask_password(ndict['driver_info'], "******")) + return { 'heartbeat_timeout': CONF.agent.heartbeat_timeout, - 'node': node.as_dict() + 'node': ndict, } def _get_completed_cleaning_command(self, task):
ironic/tests/db/utils.py+1 −0 modified@@ -152,6 +152,7 @@ def get_test_agent_driver_info(): return { 'deploy_kernel': 'glance://deploy_kernel_uuid', 'deploy_ramdisk': 'glance://deploy_ramdisk_uuid', + 'ipmi_password': 'foo', }
ironic/tests/drivers/test_agent_base_vendor.py+19 −5 modified@@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import time import types @@ -91,7 +92,8 @@ def test_lookup_version_not_found(self): @mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor' '._find_node_by_macs', autospec=True) - def test_lookup_v2(self, find_mock): + def _test_lookup_v2(self, find_mock, show_password=True): + self.context.show_password = show_password kwargs = { 'version': '2', 'inventory': { @@ -108,10 +110,20 @@ def test_lookup_v2(self, find_mock): ] } } + # NOTE(jroll) apparently as_dict() returns a dict full of references + expected = copy.deepcopy(self.node.as_dict()) + if not show_password: + expected['driver_info']['ipmi_password'] = '******' find_mock.return_value = self.node with task_manager.acquire(self.context, self.node.uuid) as task: node = self.passthru.lookup(task.context, **kwargs) - self.assertEqual(self.node.as_dict(), node['node']) + self.assertEqual(expected, node['node']) + + def test_lookup_v2_show_password(self): + self._test_lookup_v2(show_password=True) + + def test_lookup_v2_hide_password(self): + self._test_lookup_v2(show_password=False) def test_lookup_v2_missing_inventory(self): with task_manager.acquire(self.context, self.node.uuid) as task: @@ -136,9 +148,11 @@ def test_lookup_v2_empty_interfaces(self): @mock.patch.object(objects.Node, 'get_by_uuid') def test_lookup_v2_with_node_uuid(self, mock_get_node): + self.context.show_password = True + expected = copy.deepcopy(self.node.as_dict()) kwargs = { 'version': '2', - 'node_uuid': 'fake uuid', + 'node_uuid': 'fake-uuid', 'inventory': { 'interfaces': [ { @@ -156,8 +170,8 @@ def test_lookup_v2_with_node_uuid(self, mock_get_node): mock_get_node.return_value = self.node with task_manager.acquire(self.context, self.node.uuid) as task: node = self.passthru.lookup(task.context, **kwargs) - self.assertEqual(self.node.as_dict(), node['node']) - mock_get_node.assert_called_once_with(mock.ANY, 'fake uuid') + self.assertEqual(expected, node['node']) + mock_get_node.assert_called_once_with(mock.ANY, 'fake-uuid') @mock.patch.object(objects.port.Port, 'get_by_address', spec_set=types.FunctionType)
releasenotes/notes/fix-cve-2016-4985-b62abae577025365.yaml+11 −0 added@@ -0,0 +1,11 @@ +--- +security: + - A critical security vulnerability (CVE-2016-4985) was fixed in this + release. Previously, a client with network access to the ironic-api service + was able to bypass Keystone authentication and retrieve all information + about any Node registered with Ironic, if they knew (or were able to guess) + the MAC address of a network card belonging to that Node, by sending a + crafted POST request to the /v1/drivers/$DRIVER_NAME/vendor_passthru + resource. Ironic's policy.json configuration is now respected when + responding to this request such that, if passwords should be masked for + other requests, they are also masked for this request.
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
14- bugs.launchpad.net/ironic/+bug/1572796nvdVendor AdvisoryWEB
- github.com/advisories/GHSA-f7cr-7c2c-fm8rghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2016-4985ghsaADVISORY
- www.openwall.com/lists/oss-security/2016/06/21/6nvdWEB
- access.redhat.com/errata/RHSA-2016:1377nvdWEB
- access.redhat.com/errata/RHSA-2016:1378nvdWEB
- access.redhat.com/security/cve/CVE-2016-4985ghsaWEB
- bugzilla.redhat.com/show_bug.cgighsaWEB
- github.com/openstack/ironic/commit/426a306fb580762e97ada04e1253dedd9b64d410ghsaWEB
- github.com/openstack/ironic/commit/affec224977174581d19a2b914772cb0409f633eghsaWEB
- github.com/openstack/ironic/commit/f5a3ff1dfcde068769f9a2a477ba6a9edaf69c77ghsaWEB
- review.openstack.org/332195nvdWEB
- review.openstack.org/332196nvdWEB
- review.openstack.org/332197nvdWEB
News mentions
0No linked articles in our index yet.