app-admin/glance: fixing CVE-2015-5163
authorMatthew Thode <prometheanfire@gentoo.org>
Fri, 14 Aug 2015 05:22:26 +0000 (00:22 -0500)
committerMatthew Thode <prometheanfire@gentoo.org>
Fri, 14 Aug 2015 05:22:26 +0000 (00:22 -0500)
CVE: CVE-2015-5163
Signed-off-by: Matthew Thode <prometheanfire@gentoo.org>
Package-Manager: portage-2.2.20.1

app-admin/glance/files/cve-2015-5163-stable-kilo.patch [new file with mode: 0644]
app-admin/glance/glance-2015.1.1-r1.ebuild [moved from app-admin/glance/glance-2015.1.1.ebuild with 99% similarity]

diff --git a/app-admin/glance/files/cve-2015-5163-stable-kilo.patch b/app-admin/glance/files/cve-2015-5163-stable-kilo.patch
new file mode 100644 (file)
index 0000000..91507c9
--- /dev/null
@@ -0,0 +1,260 @@
+From eb99e45829a1b4c93db5692bdbf636a86faa56c4 Mon Sep 17 00:00:00 2001
+From: Flavio Percoco <flaper87@gmail.com>
+Date: Thu, 9 Jul 2015 14:44:04 +0200
+Subject: Don't import files with backed files
+
+There's a security issue where it'd be possible to import images with
+backed files using the task engine and then use/convert those to access
+system files or any other file in the system. An example of an attack
+would be to import an image with a backing file pointing to
+`/etc/passwd`, then convert it to raw and download the generated image.
+
+This patch forbids importing files with baking files entirely. It does
+that in the `_ImportToFS` task, which is the one that imports the image
+locally to then execute other tasks on it. It's not necessary for the
+`_ImportToStore` task because other tasks won't be executed when the
+image is imported in the final store.
+
+Change-Id: I35f43c3b3f326942fb53b7dadb94700ac4513494
+Closes-bug: #1471912
+(cherry picked from commit d529863a1e8d2307526bdb395b4aebe97f81603d)
+
+diff --git a/glance/async/flows/base_import.py b/glance/async/flows/base_import.py
+index 7656bde..d216aa8 100644
+--- a/glance/async/flows/base_import.py
++++ b/glance/async/flows/base_import.py
+@@ -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 @@ class _ImportToFS(task.Task):
+         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):
+diff --git a/glance/tests/unit/async/flows/test_import.py b/glance/tests/unit/async/flows/test_import.py
+index 70f790c..4cf3d13 100644
+--- a/glance/tests/unit/async/flows/test_import.py
++++ b/glance/tests/unit/async/flows/test_import.py
+@@ -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 @@ class TestImportTask(test_utils.BaseTestCase):
+         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 @@ class TestImportTask(test_utils.BaseTestCase):
+                 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 @@ class TestImportTask(test_utils.BaseTestCase):
+         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 @@ class TestImportTask(test_utils.BaseTestCase):
+         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,
+-- 
+cgit v0.10.2
+
similarity index 99%
rename from app-admin/glance/glance-2015.1.1.ebuild
rename to app-admin/glance/glance-2015.1.1-r1.ebuild
index b22016621f16a1e405e8e359dc60ffe7820d5ebb..e7df249fe714baace78a8cd31d93140ec3414c76 100644 (file)
@@ -137,6 +137,7 @@ RDEPEND="
 "
 
 PATCHES=(
+       "${FILESDIR}/cve-2015-5163-stable-kilo.patch"
 )
 
 pkg_setup() {