High severityNVD Advisory· Published Aug 19, 2015· Updated May 6, 2026
CVE-2015-5163
CVE-2015-5163
Description
The import task action in OpenStack Image Service (Glance) 2015.1.x before 2015.1.2 (kilo), when using the V2 API, allows remote authenticated users to read arbitrary files via a crafted backing file for a qcow2 image.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
glancePyPI | >= 2015.1.0, < 2015.1.2 | 2015.1.2 |
Affected products
2Patches
1eb99e45829a1Don't import files with backed files
2 files changed · +132 −32
glance/async/flows/base_import.py+26 −0 modified@@ -13,12 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. +import json import logging import os import glance_store as store_api from glance_store import backend +from oslo_concurrency import processutils as putils from oslo_config import cfg +from oslo_utils import excutils import six from stevedore import named from taskflow.patterns import linear_flow as lf @@ -146,6 +149,29 @@ def execute(self, image_id): data = script_utils.get_image_data_iter(self.uri) path = self.store.add(image_id, data, 0, context=None)[0] + + try: + # NOTE(flaper87): Consider moving this code to a common + # place that other tasks can consume as well. + stdout, stderr = putils.trycmd('qemu-img', 'info', + '--output=json', path, + log_errors=putils.LOG_ALL_ERRORS) + except OSError as exc: + with excutils.save_and_reraise_exception(): + msg = (_LE('Failed to execute security checks on the image ' + '%(task_id)s: %(exc)s') % + {'task_id': self.task_id, 'exc': exc.message}) + LOG.error(msg) + + metadata = json.loads(stdout) + + backing_file = metadata.get('backing-filename') + if backing_file is not None: + msg = _("File %(path)s has invalid backing file " + "%(bfile)s, aborting.") % {'path': path, + 'bfile': backing_file} + raise RuntimeError(msg) + return path def revert(self, image_id, result=None, **kwargs):
glance/tests/unit/async/flows/test_import.py+106 −32 modified@@ -13,14 +13,17 @@ # License for the specific language governing permissions and limitations # under the License. +import json import mock import os import urllib2 import glance_store +from oslo_concurrency import processutils as putils from oslo_config import cfg from six.moves import cStringIO from taskflow import task +from taskflow.types import failure import glance.async.flows.base_import as import_flow from glance.async import taskflow_executor @@ -106,16 +109,23 @@ def create_image(*args, **kwargs): with mock.patch.object(script_utils, 'get_image_data_iter') as dmock: dmock.return_value = cStringIO("TEST_IMAGE") - executor.begin_processing(self.task.task_id) - image_path = os.path.join(self.test_dir, self.image.image_id) - tmp_image_path = os.path.join(self.work_dir, - "%s.tasks_import" % image_path) - self.assertFalse(os.path.exists(tmp_image_path)) - self.assertTrue(os.path.exists(image_path)) - self.assertEqual(1, len(list(self.image.locations))) - self.assertEqual("file://%s/%s" % (self.test_dir, - self.image.image_id), - self.image.locations[0]['url']) + + with mock.patch.object(putils, 'trycmd') as tmock: + tmock.return_value = (json.dumps({ + 'format': 'qcow2', + }), None) + + executor.begin_processing(self.task.task_id) + image_path = os.path.join(self.test_dir, self.image.image_id) + tmp_image_path = os.path.join(self.work_dir, + "%s.tasks_import" % image_path) + + self.assertFalse(os.path.exists(tmp_image_path)) + self.assertTrue(os.path.exists(image_path)) + self.assertEqual(1, len(list(self.image.locations))) + self.assertEqual("file://%s/%s" % (self.test_dir, + self.image.image_id), + self.image.locations[0]['url']) def test_import_flow_missing_work_dir(self): self.config(engine_mode='serial', group='taskflow_executor') @@ -151,6 +161,54 @@ def create_image(*args, **kwargs): self.assertFalse(os.path.exists(tmp_image_path)) self.assertTrue(os.path.exists(image_path)) + def test_import_flow_backed_file_import_to_fs(self): + self.config(engine_mode='serial', group='taskflow_executor') + + img_factory = mock.MagicMock() + + executor = taskflow_executor.TaskExecutor( + self.context, + self.task_repo, + self.img_repo, + img_factory) + + self.task_repo.get.return_value = self.task + + def create_image(*args, **kwargs): + kwargs['image_id'] = UUID1 + return self.img_factory.new_image(*args, **kwargs) + + self.img_repo.get.return_value = self.image + img_factory.new_image.side_effect = create_image + + with mock.patch.object(script_utils, 'get_image_data_iter') as dmock: + dmock.return_value = cStringIO("TEST_IMAGE") + + with mock.patch.object(putils, 'trycmd') as tmock: + tmock.return_value = (json.dumps({ + 'backing-filename': '/etc/password' + }), None) + + with mock.patch.object(import_flow._ImportToFS, + 'revert') as rmock: + self.assertRaises(RuntimeError, + executor.begin_processing, + self.task.task_id) + self.assertTrue(rmock.called) + self.assertIsInstance(rmock.call_args[1]['result'], + failure.Failure) + + image_path = os.path.join(self.test_dir, + self.image.image_id) + + fname = "%s.tasks_import" % image_path + tmp_image_path = os.path.join(self.work_dir, fname) + + self.assertFalse(os.path.exists(tmp_image_path)) + # Note(sabari): The image should not have been uploaded to + # the store as the flow failed before ImportToStore Task. + self.assertFalse(os.path.exists(image_path)) + def test_import_flow_revert(self): self.config(engine_mode='serial', group='taskflow_executor') @@ -175,20 +233,31 @@ def create_image(*args, **kwargs): with mock.patch.object(script_utils, 'get_image_data_iter') as dmock: dmock.return_value = cStringIO("TEST_IMAGE") - with mock.patch.object(import_flow, "_get_import_flows") as imock: - imock.return_value = (x for x in [_ErrorTask()]) - self.assertRaises(RuntimeError, - executor.begin_processing, self.task.task_id) - image_path = os.path.join(self.test_dir, self.image.image_id) - tmp_image_path = os.path.join(self.work_dir, - "%s.tasks_import" % image_path) - self.assertFalse(os.path.exists(tmp_image_path)) - - # NOTE(flaper87): Eventually, we want this to be assertTrue. - # The current issue is there's no way to tell taskflow to - # continue on failures. That is, revert the subflow but keep - # executing the parent flow. Under discussion/development. - self.assertFalse(os.path.exists(image_path)) + with mock.patch.object(putils, 'trycmd') as tmock: + tmock.return_value = (json.dumps({ + 'format': 'qcow2', + }), None) + + with mock.patch.object(import_flow, + "_get_import_flows") as imock: + imock.return_value = (x for x in [_ErrorTask()]) + self.assertRaises(RuntimeError, + executor.begin_processing, + self.task.task_id) + + image_path = os.path.join(self.test_dir, + self.image.image_id) + tmp_image_path = os.path.join(self.work_dir, + ("%s.tasks_import" % + image_path)) + self.assertFalse(os.path.exists(tmp_image_path)) + + # NOTE(flaper87): Eventually, we want this to be assertTrue + # The current issue is there's no way to tell taskflow to + # continue on failures. That is, revert the subflow but + # keep executing the parent flow. Under + # discussion/development. + self.assertFalse(os.path.exists(image_path)) def test_import_flow_no_import_flows(self): self.config(engine_mode='serial', @@ -271,15 +340,20 @@ def test_import_to_fs(self): with mock.patch.object(script_utils, 'get_image_data_iter') as dmock: dmock.return_value = "test" - image_id = UUID1 - path = import_fs.execute(image_id) - reader, size = glance_store.get_from_backend(path) - self.assertEqual(4, size) - self.assertEqual(dmock.return_value, "".join(reader)) + with mock.patch.object(putils, 'trycmd') as tmock: + tmock.return_value = (json.dumps({ + 'format': 'qcow2', + }), None) + + image_id = UUID1 + path = import_fs.execute(image_id) + reader, size = glance_store.get_from_backend(path) + self.assertEqual(4, size) + self.assertEqual(dmock.return_value, "".join(reader)) - image_path = os.path.join(self.work_dir, image_id) - tmp_image_path = os.path.join(self.work_dir, image_path) - self.assertTrue(os.path.exists(tmp_image_path)) + image_path = os.path.join(self.work_dir, image_id) + tmp_image_path = os.path.join(self.work_dir, image_path) + self.assertTrue(os.path.exists(tmp_image_path)) def test_delete_from_fs(self): delete_fs = import_flow._DeleteFromFS(self.task.task_id,
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
12- github.com/advisories/GHSA-q73f-vjc2-3gqfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2015-5163ghsaADVISORY
- lists.openstack.org/pipermail/openstack-announce/2015-August/000527.htmlnvdWEB
- rhn.redhat.com/errata/RHSA-2015-1639.htmlnvdWEB
- access.redhat.com/errata/RHSA-2015:1639ghsaWEB
- access.redhat.com/security/cve/CVE-2015-5163ghsaWEB
- bugs.launchpad.net/glance/+bug/1471912nvdWEB
- bugzilla.redhat.com/show_bug.cgighsaWEB
- github.com/openstack/glance/commit/eb99e45829a1b4c93db5692bdbf636a86faa56c4ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/glance/PYSEC-2015-39.yamlghsaWEB
- web.archive.org/web/20200228024903/http://www.securityfocus.com/bid/76346ghsaWEB
- www.securityfocus.com/bid/76346nvd
News mentions
0No linked articles in our index yet.