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

PackageAffected versionsPatched versions
ironicPyPI
< 4.2.54.2.5
ironicPyPI
>= 5.0, < 5.1.25.1.2

Affected products

5
  • Red Hat/Openstack2 versions
    cpe:2.3:a:redhat:openstack:7.0:*:*:*:*:*:*:*+ 1 more
    • cpe:2.3:a:redhat:openstack:7.0:*:*:*:*:*:*:*
    • cpe:2.3:a:redhat:openstack:8:*:*:*:*:*:*:*
  • cpe: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

3
426a306fb580

Mask password on agent lookup according to policy

https://github.com/openstack/ironicDevananda van der VeenJun 11, 2016via ghsa
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. 
    
affec2249771

Mask password on agent lookup according to policy

https://github.com/openstack/ironicDevananda van der VeenJun 11, 2016via ghsa
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. 
    
f5a3ff1dfcde

Mask password on agent lookup according to policy

https://github.com/openstack/ironicDevananda van der VeenJun 11, 2016via ghsa
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

News mentions

0

No linked articles in our index yet.