prometheanfire 15/01/14 02:46:07 Added: 0001-To-prevent-client-use-v2-patch-api-to-handle-file-an.patch 0001-Prevent-file-swift-config-and-filesystem-schemes.patch Log: fixing CVE-2014-9493 (Portage version: 2.2.14/cvs/Linux x86_64, signed Manifest commit with key 0x33ED3FD25AFC78BA)
Revision Changes Path 1.1 app-admin/glance/files/0001-To-prevent-client-use-v2-patch-api-to-handle-file-an.patch file : http://sources.gentoo.org/viewvc.cgi/gentoo-x86/app-admin/glance/files/0001-To-prevent-client-use-v2-patch-api-to-handle-file-an.patch?rev=1.1&view=markup plain: http://sources.gentoo.org/viewvc.cgi/gentoo-x86/app-admin/glance/files/0001-To-prevent-client-use-v2-patch-api-to-handle-file-an.patch?rev=1.1&content-type=text/plain Index: 0001-To-prevent-client-use-v2-patch-api-to-handle-file-an.patch =================================================================== >From d9a928eac360add67477e29f516af868adfe0d5e Mon Sep 17 00:00:00 2001 From: Zhi Yan Liu <zhiy...@cn.ibm.com> Date: Mon, 15 Dec 2014 12:29:55 +0800 Subject: [PATCH] To prevent client use v2 patch api to handle file and swift location The change will be used to restrict client to download and delete any file in glance-api server. The same resone and logic as what we did in v1: https://github.com/openstack/glance/blob/master/glance/api/v1/images.py#L429 Closes-Bug: bug/1400966 DocImpact Note: Even this change could fully resolve the problem for Glance, but we still need to fix this issue from glance_store perspective separatelly due to other projects can use the lib directly. Conflicts: glance/api/v1/images.py glance/location.py glance/tests/functional/v2/test_images.py glance/tests/unit/test_store_location.py glance/tests/unit/v1/test_api.py (cherry-picked from 4afdb017aa1ccef01482f117cb8d0736a6da38ed) Signed-off-by: Zhi Yan Liu <zhiy...@cn.ibm.com> Change-Id: I72dbead3cb2dcb87f52658ddb880e26880cc229b --- glance/api/v1/images.py | 31 ++--- glance/common/store_utils.py | 22 ++++ glance/location.py | 30 +++-- glance/tests/functional/v1/test_copy_to_file.py | 4 +- glance/tests/functional/v2/test_images.py | 158 ++++++++++-------------- glance/tests/unit/test_store_image.py | 3 +- glance/tests/unit/test_store_location.py | 33 ++++- glance/tests/unit/utils.py | 9 +- glance/tests/unit/v1/test_api.py | 62 +++++++++- 9 files changed, 221 insertions(+), 131 deletions(-) diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py index c85b301..746f8cd 100644 --- a/glance/api/v1/images.py +++ b/glance/api/v1/images.py @@ -23,7 +23,6 @@ import eventlet import glance_store as store import glance_store.location from oslo.config import cfg -import six.moves.urllib.parse as urlparse from webob.exc import HTTPBadRequest from webob.exc import HTTPConflict from webob.exc import HTTPForbidden @@ -40,6 +39,7 @@ from glance.api.v1 import filters from glance.api.v1 import upload_utils from glance.common import exception from glance.common import property_utils +from glance.common import store_utils from glance.common import utils from glance.common import wsgi from glance.i18n import _LE @@ -415,26 +415,19 @@ class Controller(controller.BaseController): @staticmethod def _validate_source(source, req): """ - External sources (as specified via the location or copy-from headers) - are supported only over non-local store types, i.e. S3, Swift, HTTP. - Note the absence of 'file://' for security reasons, see LP bug #942118. - 'swift+config://' is also absent for security reasons, see LP bug - #1334196. - If the above constraint is violated, we reject with 400 "Bad Request". + To validate if external sources (as specified via the location + or copy-from headers) are supported. Otherwise we reject + with 400 "Bad Request". """ if source: - pieces = urlparse.urlparse(source) - schemes = [scheme for scheme in store.get_known_schemes() - if scheme != 'file' and scheme != 'swift+config'] - for scheme in schemes: - if pieces.scheme == scheme: - return source - msg = ("External sourcing not supported for " - "store '%s'" % pieces.scheme) - LOG.debug(msg) - raise HTTPBadRequest(explanation=msg, - request=req, - content_type="text/plain") + if store_utils.validate_external_location(source): + return source + else: + msg = _("External source are not supported: '%s'") % source + LOG.debug(msg) + raise HTTPBadRequest(explanation=msg, + request=req, + content_type="text/plain") @staticmethod def _copy_from(req): diff --git a/glance/common/store_utils.py b/glance/common/store_utils.py index 8f04d39..b7537ce 100644 --- a/glance/common/store_utils.py +++ b/glance/common/store_utils.py @@ -16,6 +16,7 @@ import sys import glance_store as store_api from oslo.config import cfg +import six.moves.urllib.parse as urlparse from glance.common import utils import glance.db as db_api @@ -119,3 +120,24 @@ def delete_image_location_from_backend(context, image_id, location): # such as uploading process failure then we can't use # location status mechanism to support image pending delete. safe_delete_from_backend(context, image_id, location) + + +def validate_external_location(uri): + """ + Validate if URI of external location are supported. + + Only over non-local store types are OK, i.e. S3, Swift, + HTTP. Note the absence of 'file://' for security reasons, + see LP bug #942118, 1400966, 'swift+config://' is also + absent for security reasons, see LP bug #1334196. + + :param uri: The URI of external image location. + :return: Whether given URI of external image location are OK. + """ + + # TODO(zhiyan): This function could be moved to glance_store. + + pieces = urlparse.urlparse(uri) + valid_schemes = [scheme for scheme in store_api.get_known_schemes() + if scheme != 'file' and scheme != 'swift+config'] + return pieces.scheme in valid_schemes diff --git a/glance/location.py b/glance/location.py index fcdba0a..f83fa7a 100644 --- a/glance/location.py +++ b/glance/location.py @@ -66,18 +66,20 @@ class ImageRepoProxy(glance.domain.proxy.Repo): return result -def _check_location_uri(context, store_api, uri): +def _check_location_uri(context, store_api, store_utils, uri): """Check if an image location is valid. :param context: Glance request context :param store_api: store API module + :param store_utils: store utils module :param uri: location's uri string """ + is_ok = True try: - size = store_api.get_size_from_backend(uri, context=context) # NOTE(zhiyan): Some stores return zero when it catch exception - is_ok = size > 0 + is_ok = (store_utils.validate_external_location(uri) and + store_api.get_size_from_backend(uri, context=context) > 0) except (store.UnknownScheme, store.NotFound): is_ok = False if not is_ok: @@ -85,8 +87,8 @@ def _check_location_uri(context, store_api, uri): raise exception.BadStoreUri(message=reason) -def _check_image_location(context, store_api, location): - _check_location_uri(context, store_api, location['url']) +def _check_image_location(context, store_api, store_utils, location): + _check_location_uri(context, store_api, store_utils, location['url']) store_api.check_location_metadata(location['metadata']) @@ -122,6 +124,7 @@ class ImageFactoryProxy(glance.domain.proxy.ImageFactory): def __init__(self, factory, context, store_api, store_utils): self.context = context self.store_api = store_api + self.store_utils = store_utils proxy_kwargs = {'context': context, 'store_api': store_api, 'store_utils': store_utils} super(ImageFactoryProxy, self).__init__(factory, @@ -131,7 +134,10 @@ class ImageFactoryProxy(glance.domain.proxy.ImageFactory): def new_image(self, **kwargs): locations = kwargs.get('locations', []) for loc in locations: - _check_image_location(self.context, self.store_api, loc) + _check_image_location(self.context, + self.store_api, + self.store_utils, + loc) loc['status'] = 'active' if _count_duplicated_locations(locations, loc) > 1: raise exception.DuplicateLocation(location=loc['url']) @@ -169,7 +175,9 @@ class StoreLocations(collections.MutableSequence): def insert(self, i, location): _check_image_location(self.image_proxy.context, - self.image_proxy.store_api, location) + self.image_proxy.store_api, + self.image_proxy.store_utils, + location) location['status'] = 'active' if _count_duplicated_locations(self.value, location) > 0: raise exception.DuplicateLocation(location=location['url']) @@ -214,7 +222,9 @@ class StoreLocations(collections.MutableSequence): def __setitem__(self, i, location): _check_image_location(self.image_proxy.context, - self.image_proxy.store_api, location) + self.image_proxy.store_api, + self.image_proxy.store_utils, + location) location['status'] = 'active' self.value.__setitem__(i, location) _set_image_size(self.image_proxy.context, @@ -303,7 +313,9 @@ def _locations_proxy(target, attr): '%s') % ori_value) # NOTE(zhiyan): Check locations are all valid. for location in value: - _check_image_location(self.context, self.store_api, + _check_image_location(self.context, + self.store_api, + self.store_utils, location) location['status'] = 'active' if _count_duplicated_locations(value, location) > 1: diff --git a/glance/tests/functional/v1/test_copy_to_file.py b/glance/tests/functional/v1/test_copy_to_file.py index 15bb708..b64eac6 100644 --- a/glance/tests/functional/v1/test_copy_to_file.py +++ b/glance/tests/functional/v1/test_copy_to_file.py @@ -250,7 +250,7 @@ class TestCopyToFile(functional.FunctionalTest): response, content = http.request(path, 'POST', headers=headers) self.assertEqual(response.status, 400, content) - expected = 'External sourcing not supported for store \'file\'' + expected = 'External source are not supported: \'%s\'' % copy_from msg = 'expected "%s" in "%s"' % (expected, content) self.assertTrue(expected in content, msg) @@ -276,7 +276,7 @@ class TestCopyToFile(functional.FunctionalTest): response, content = http.request(path, 'POST', headers=headers) self.assertEqual(response.status, 400, content) - expected = 'External sourcing not supported for store \'swift+config\'' + expected = 'External source are not supported: \'swift+config://xxx\'' msg = 'expected "%s" in "%s"' % (expected, content) self.assertTrue(expected in content, msg) diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py index 14fe3c7..4c32375 100644 --- a/glance/tests/functional/v2/test_images.py +++ b/glance/tests/functional/v2/test_images.py @@ -16,7 +16,6 @@ import BaseHTTPServer import os import signal -import tempfile import uuid import requests @@ -47,7 +46,7 @@ def get_handler_class(fixture): self.end_headers() return - def log_message(*args, **kwargs): + def log_message(self, *args, **kwargs): # Override this method to prevent debug output from going # to stderr during testing return @@ -75,6 +74,18 @@ class TestImages(functional.FunctionalTest): self.cleanup() self.api_server.deployment_flavor = 'noauth' self.api_server.data_api = 'glance.db.sqlalchemy.api' + for i in range(3): + ret = http_server("foo_image_id%d" % i, "foo_image%d" % i) + setattr(self, 'http_server%d_pid' % i, ret[0]) + setattr(self, 'http_port%d' % i, ret[1]) + + def tearDown(self): + for i in range(3): + pid = getattr(self, 'http_server%d_pid' % i, None) + if pid: + os.kill(pid, signal.SIGKILL) + + super(TestImages, self).tearDown() def _url(self, path): return 'http://127.0.0.1:%d%s' % (self.api_port, path) @@ -329,21 +340,15 @@ class TestImages(functional.FunctionalTest): self.assertEqual(413, response.status_code, response.text) # Adding 3 image locations should fail since configured limit is 2 - for i in range(3): - file_path = os.path.join(self.test_dir, 'fake_image_%i' % i) - with open(file_path, 'w') as fap: - fap.write('glance') - path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) changes = [] for i in range(3): + url = ('http://127.0.0.1:%s/foo_image' % + getattr(self, 'http_port%d' % i)) changes.append({'op': 'add', 'path': '/locations/-', - 'value': {'url': 'file://{0}'.format( - os.path.join(self.test_dir, - 'fake_image_%i' % i)), - 'metadata': {}}, + 'value': {'url': url, 'metadata': {}}, }) data = jsonutils.dumps(changes) @@ -2176,17 +2181,14 @@ class TestImages(functional.FunctionalTest): self.assertNotIn('size', image) self.assertNotIn('virtual_size', image) - file_path = os.path.join(self.test_dir, 'fake_image') - with open(file_path, 'w') as fap: - fap.write('glance') - # Update locations for the queued image path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) + url = 'http://127.0.0.1:%s/foo_image' % self.http_port0 data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', - 'value': [{'url': 'file://' + file_path, - 'metadata': {}}]}]) + 'value': [{'url': url, 'metadata': {}}] + }]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(200, response.status_code, response.text) @@ -2195,7 +2197,42 @@ class TestImages(functional.FunctionalTest): response = requests.get(path, headers=headers) self.assertEqual(200, response.status_code) image = jsonutils.loads(response.text) - self.assertEqual(image['size'], 6) + self.assertEqual(image['size'], 10) + + def test_update_locations_with_restricted_sources(self): + self.start_servers(**self.__dict__.copy()) + # Create an image + path = self._url('/v2/images') + headers = self._headers({'content-type': 'application/json'}) + data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', + 'container_format': 'aki'}) + response = requests.post(path, headers=headers, data=data) + self.assertEqual(201, response.status_code) + + # Returned image entity should have a generated id and status + image = jsonutils.loads(response.text) + image_id = image['id'] + self.assertEqual('queued', image['status']) + self.assertNotIn('size', image) + self.assertNotIn('virtual_size', image) + + # Update locations for the queued image + path = self._url('/v2/images/%s' % image_id) + media_type = 'application/openstack-images-v2.1-json-patch' + headers = self._headers({'content-type': media_type}) + data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', + 'value': [{'url': 'file:///foo_image', + 'metadata': {}}] + }]) + response = requests.patch(path, headers=headers, data=data) + self.assertEqual(400, response.status_code, response.text) + + data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', + 'value': [{'url': 'swift+config:///foo_image', + 'metadata': {}}] + }]) + response = requests.patch(path, headers=headers, data=data) + self.assertEqual(400, response.status_code, response.text) class TestImagesWithRegistry(TestImages): @@ -2421,16 +2458,16 @@ class TestImageLocationSelectionStrategy(functional.FunctionalTest): super(TestImageLocationSelectionStrategy, self).setUp() self.cleanup() self.api_server.deployment_flavor = 'noauth' - self.foo_image_file = tempfile.NamedTemporaryFile() - self.foo_image_file.write("foo image file") - self.foo_image_file.flush() - self.addCleanup(self.foo_image_file.close) - ret = http_server("foo_image_id", "foo_image") - self.http_server_pid, self.http_port = ret + for i in range(3): + ret = http_server("foo_image_id%d" % i, "foo_image%d" % i) + setattr(self, 'http_server%d_pid' % i, ret[0]) + setattr(self, 'http_port%d' % i, ret[1]) def tearDown(self): - if self.http_server_pid is not None: - os.kill(self.http_server_pid, signal.SIGKILL) + for i in range(3): + pid = getattr(self, 'http_server%d_pid' % i, None) + if pid: + os.kill(pid, signal.SIGKILL) super(TestImageLocationSelectionStrategy, self).tearDown() @@ -2483,69 +2520,10 @@ class TestImageLocationSelectionStrategy(functional.FunctionalTest): path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) - values = [{'url': 'file://%s' % self.foo_image_file.name, - 'metadata': {'idx': '1'}}, - {'url': 'http://127.0.0.1:%s/foo_image' % self.http_port, - 'metadata': {'idx': '0'}}] - doc = [{'op': 'replace', - 'path': '/locations', - 'value': values}] - data = jsonutils.dumps(doc) - response = requests.patch(path, headers=headers, data=data) - self.assertEqual(200, response.status_code) - - # Image locations should be visible - path = self._url('/v2/images/%s' % image_id) - headers = self._headers({'Content-Type': 'application/json'}) - response = requests.get(path, headers=headers) - self.assertEqual(200, response.status_code) - image = jsonutils.loads(response.text) - self.assertTrue('locations' in image) - self.assertEqual(image['locations'], values) - self.assertTrue('direct_url' in image) - self.assertEqual(image['direct_url'], values[0]['url']) - - self.stop_servers() - - def test_image_locatons_with_store_type_strategy(self): - self.api_server.show_image_direct_url = True - self.api_server.show_multiple_locations = True - self.image_location_quota = 10 - self.api_server.location_strategy = 'store_type' - preference = "http, swift, filesystem" - self.api_server.store_type_location_strategy_preference = preference - self.start_servers(**self.__dict__.copy()) - - # Create an image - path = self._url('/v2/images') - headers = self._headers({'content-type': 'application/json'}) - data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', - 'foo': 'bar', 'disk_format': 'aki', - 'container_format': 'aki'}) - response = requests.post(path, headers=headers, data=data) - self.assertEqual(201, response.status_code) - - # Get the image id - image = jsonutils.loads(response.text) - image_id = image['id'] - - # Image locations should not be visible before location is set - path = self._url('/v2/images/%s' % image_id) - headers = self._headers({'Content-Type': 'application/json'}) - response = requests.get(path, headers=headers) - self.assertEqual(200, response.status_code) - image = jsonutils.loads(response.text) - self.assertTrue('locations' in image) - self.assertTrue(image["locations"] == []) - - # Update image locations via PATCH - path = self._url('/v2/images/%s' % image_id) - media_type = 'application/openstack-images-v2.1-json-patch' - headers = self._headers({'content-type': media_type}) - values = [{'url': 'file://%s' % self.foo_image_file.name, - 'metadata': {'idx': '1'}}, - {'url': 'http://127.0.0.1:%s/foo_image' % self.http_port, - 'metadata': {'idx': '0'}}] + values = [{'url': 'http://127.0.0.1:%s/foo_image' % self.http_port0, + 'metadata': {}}, + {'url': 'http://127.0.0.1:%s/foo_image' % self.http_port1, + 'metadata': {}}] doc = [{'op': 'replace', 'path': '/locations', 'value': values}] @@ -2553,8 +2531,6 @@ class TestImageLocationSelectionStrategy(functional.FunctionalTest): response = requests.patch(path, headers=headers, data=data) self.assertEqual(200, response.status_code) - values.sort(key=lambda loc: int(loc['metadata']['idx'])) - # Image locations should be visible path = self._url('/v2/images/%s' % image_id) headers = self._headers({'Content-Type': 'application/json'}) diff --git a/glance/tests/unit/test_store_image.py b/glance/tests/unit/test_store_image.py index 665f126..8b334ab 100644 --- a/glance/tests/unit/test_store_image.py +++ b/glance/tests/unit/test_store_image.py @@ -18,6 +18,7 @@ import glance_store from glance.common import exception import glance.location +from glance.tests.unit import base as unit_test_base from glance.tests.unit import utils as unit_test_utils from glance.tests import utils @@ -759,7 +760,7 @@ class TestStoreImageRepo(utils.BaseTestCase): self.assertEqual(acls['read'], [TENANT2]) -class TestImageFactory(utils.BaseTestCase): +class TestImageFactory(unit_test_base.StoreClearingUnitTest): def setUp(self): super(TestImageFactory, self).setUp() diff --git a/glance/tests/unit/test_store_location.py b/glance/tests/unit/test_store_location.py index 884221b..c9ee44c 100644 --- a/glance/tests/unit/test_store_location.py +++ b/glance/tests/unit/test_store_location.py @@ -17,6 +17,8 @@ import mock import glance_store +from glance.common import exception +from glance.common import store_utils import glance.location from glance.tests.unit import base @@ -32,11 +34,13 @@ CONF = {'default_store': 'file', class TestStoreLocation(base.StoreClearingUnitTest): + class FakeImageProxy(): + size = None + context = None + store_api = mock.Mock() + store_utils = store_utils + def test_add_location_for_image_without_size(self): - class FakeImageProxy(): - size = None - context = None - store_api = mock.Mock() def fake_get_size_from_backend(uri, context=None): return 1 @@ -49,14 +53,31 @@ class TestStoreLocation(base.StoreClearingUnitTest): loc2 = {'url': 'file:///fake2.img.tar.gz', 'metadata': {}} # Test for insert location - image1 = FakeImageProxy() + image1 = TestStoreLocation.FakeImageProxy() locations = glance.location.StoreLocations(image1, []) locations.insert(0, loc2) self.assertEqual(image1.size, 1) # Test for set_attr of _locations_proxy - image2 = FakeImageProxy() + image2 = TestStoreLocation.FakeImageProxy() locations = glance.location.StoreLocations(image2, [loc1]) locations[0] = loc2 self.assertIn(loc2, locations) self.assertEqual(image2.size, 1) + + def test_add_location_with_restricted_sources(self): + + loc1 = {'url': 'file:///fake1.img.tar.gz', 'metadata': {}} + loc2 = {'url': 'swift+config:///xxx', 'metadata': {}} + + # Test for insert location + image1 = TestStoreLocation.FakeImageProxy() + locations = glance.location.StoreLocations(image1, []) + self.assertRaises(exception.BadStoreUri, locations.insert, 0, loc1) + self.assertNotIn(loc1, locations) + + # Test for set_attr of _locations_proxy + image2 = TestStoreLocation.FakeImageProxy() + locations = glance.location.StoreLocations(image2, [loc1]) + self.assertRaises(exception.BadStoreUri, locations.insert, 0, loc2) + self.assertNotIn(loc2, locations) diff --git a/glance/tests/unit/utils.py b/glance/tests/unit/utils.py index df59160..f7c8d56 100644 --- a/glance/tests/unit/utils.py +++ b/glance/tests/unit/utils.py @@ -14,12 +14,13 @@ # under the License. import urllib -import urlparse import glance_store as store from oslo.config import cfg +import six.moves.urllib.parse as urlparse from glance.common import exception +from glance.common import store_utils from glance.common import wsgi import glance.context import glance.db.simple.api as simple_db @@ -135,6 +136,12 @@ class FakeStoreUtils(object): else: self.safe_delete_from_backend(context, image_id, location) + def validate_external_location(self, uri): + if uri and urlparse.urlparse(uri).scheme: + return store_utils.validate_external_location(uri) + else: + return True + class FakeStoreAPI(object): def __init__(self, store_metadata=None): diff --git a/glance/tests/unit/v1/test_api.py b/glance/tests/unit/v1/test_api.py index bd2182e..4ec136d 100644 --- a/glance/tests/unit/v1/test_api.py +++ b/glance/tests/unit/v1/test_api.py @@ -419,7 +419,7 @@ class TestGlanceAPI(base.IsolatedUnitTest): res = req.get_response(self.api) self.assertEqual(res.status_int, 400) - self.assertIn('External sourcing not supported', res.body) + self.assertIn('External source are not supported', res.body) def test_create_with_location_bad_store_uri(self): fixture_headers = { @@ -1006,7 +1006,7 @@ class TestGlanceAPI(base.IsolatedUnitTest): res = req.get_response(self.api) self.assertEqual(res.status_int, 409) - def test_add_location_with_invalid_location(self): + def test_add_location_with_invalid_location_on_conflict_image_size(self): """Tests creates an image from location and conflict image size""" fixture_headers = {'x-image-meta-store': 'file', 'x-image-meta-disk-format': 'vhd', @@ -1023,6 +1023,36 @@ class TestGlanceAPI(base.IsolatedUnitTest): res = req.get_response(self.api) self.assertEqual(res.status_int, 400) + def test_add_location_with_invalid_location_on_restricted_sources(self): + """Tests creates an image from location and restricted sources""" + fixture_headers = {'x-image-meta-store': 'file', + 'x-image-meta-disk-format': 'vhd', + 'x-image-meta-location': 'file:///etc/passwd', + 'x-image-meta-container-format': 'ovf', + 'x-image-meta-name': 'fake image #F'} + + req = webob.Request.blank("/images") + req.headers['Content-Type'] = 'application/octet-stream' + req.method = 'POST' + for k, v in fixture_headers.iteritems(): + req.headers[k] = v + res = req.get_response(self.api) + self.assertEqual(400, res.status_int) + + fixture_headers = {'x-image-meta-store': 'file', + 'x-image-meta-disk-format': 'vhd', + 'x-image-meta-location': 'swift+config://xxx', + 'x-image-meta-container-format': 'ovf', + 'x-image-meta-name': 'fake image #F'} + + req = webob.Request.blank("/images") + req.headers['Content-Type'] = 'application/octet-stream' + req.method = 'POST' + for k, v in fixture_headers.iteritems(): + req.headers[k] = v + res = req.get_response(self.api) + self.assertEqual(400, res.status_int) + def test_add_copy_from_with_location(self): """Tests creates an image from copy-from and location""" fixture_headers = {'x-image-meta-store': 'file', @@ -1039,6 +1069,34 @@ class TestGlanceAPI(base.IsolatedUnitTest): res = req.get_response(self.api) self.assertEqual(res.status_int, 400) + def test_add_copy_from_with_restricted_sources(self): + """Tests creates an image from copy-from with restricted sources""" + fixture_headers = {'x-image-meta-store': 'file', + 'x-image-meta-disk-format': 'vhd', + 'x-glance-api-copy-from': 'file:///etc/passwd', + 'x-image-meta-container-format': 'ovf', + 'x-image-meta-name': 'fake image #F'} + + req = webob.Request.blank("/images") + req.method = 'POST' + for k, v in six.iteritems(fixture_headers): + req.headers[k] = v + res = req.get_response(self.api) + self.assertEqual(400, res.status_int) + + fixture_headers = {'x-image-meta-store': 'file', + 'x-image-meta-disk-format': 'vhd', + 'x-glance-api-copy-from': 'swift+config://xxx', + 'x-image-meta-container-format': 'ovf', + 'x-image-meta-name': 'fake image #F'} + + req = webob.Request.blank("/images") + req.method = 'POST' + for k, v in six.iteritems(fixture_headers): + req.headers[k] = v + res = req.get_response(self.api) + self.assertEqual(400, res.status_int) + def test_add_copy_from_upload_image_unauthorized_with_body(self): rules = {"upload_image": '!', "modify_image": '@', "add_image": '@'} -- 2.0.5 1.1 app-admin/glance/files/0001-Prevent-file-swift-config-and-filesystem-schemes.patch file : http://sources.gentoo.org/viewvc.cgi/gentoo-x86/app-admin/glance/files/0001-Prevent-file-swift-config-and-filesystem-schemes.patch?rev=1.1&view=markup plain: http://sources.gentoo.org/viewvc.cgi/gentoo-x86/app-admin/glance/files/0001-Prevent-file-swift-config-and-filesystem-schemes.patch?rev=1.1&content-type=text/plain Index: 0001-Prevent-file-swift-config-and-filesystem-schemes.patch =================================================================== >From 5191ed1879c5fd5b2694f922bcedec232f461088 Mon Sep 17 00:00:00 2001 From: Grant Murphy <grant.mur...@hp.com> Date: Wed, 7 Jan 2015 16:09:38 -0800 Subject: [PATCH] Prevent file, swift+config and filesystem schemes This change ensures that 'file', 'filesystem', and 'swift+config' URI schemes are not allowed when setting the location field. A previous fix to CVE-2014-9493 attempted to address this issue but did not include 'filesystem', a URI scheme allowed by the glance_store. Without this fix in place it is possible for a client to access any file the glance-api server has read permissions for. Change-Id: I02cd099a8634b9c7e3cf8f172bcbd33f8edcbc83 Closes-Bug: #1408663 (cherry picked from commit a2d986b976e9325a272e2d422465165315d19fe6) --- glance/common/store_utils.py | 11 ++++++----- glance/tests/unit/test_store_location.py | 3 +++ glance/tests/unit/v1/test_api.py | 32 ++++++++++++-------------------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/glance/common/store_utils.py b/glance/common/store_utils.py index b7537ce..64cfa87 100644 --- a/glance/common/store_utils.py +++ b/glance/common/store_utils.py @@ -38,6 +38,8 @@ store_utils_opts = [ CONF = cfg.CONF CONF.register_opts(store_utils_opts) +RESTRICTED_URI_SCHEMAS = frozenset(['file', 'filesystem', 'swift+config']) + def safe_delete_from_backend(context, image_id, location): """ @@ -136,8 +138,7 @@ def validate_external_location(uri): """ # TODO(zhiyan): This function could be moved to glance_store. - - pieces = urlparse.urlparse(uri) - valid_schemes = [scheme for scheme in store_api.get_known_schemes() - if scheme != 'file' and scheme != 'swift+config'] - return pieces.scheme in valid_schemes + # TODO(gm): Use a whitelist of allowed schemes + scheme = urlparse.urlparse(uri).scheme + return (scheme in store_api.get_known_schemes() and + scheme not in RESTRICTED_URI_SCHEMAS) diff --git a/glance/tests/unit/test_store_location.py b/glance/tests/unit/test_store_location.py index c9ee44c..efaecd8 100644 --- a/glance/tests/unit/test_store_location.py +++ b/glance/tests/unit/test_store_location.py @@ -69,12 +69,15 @@ class TestStoreLocation(base.StoreClearingUnitTest): loc1 = {'url': 'file:///fake1.img.tar.gz', 'metadata': {}} loc2 = {'url': 'swift+config:///xxx', 'metadata': {}} + loc3 = {'url': 'filesystem:///foo.img.tar.gz', 'metadata': {}} # Test for insert location image1 = TestStoreLocation.FakeImageProxy() locations = glance.location.StoreLocations(image1, []) self.assertRaises(exception.BadStoreUri, locations.insert, 0, loc1) + self.assertRaises(exception.BadStoreUri, locations.insert, 0, loc3) self.assertNotIn(loc1, locations) + self.assertNotIn(loc3, locations) # Test for set_attr of _locations_proxy image2 = TestStoreLocation.FakeImageProxy() diff --git a/glance/tests/unit/v1/test_api.py b/glance/tests/unit/v1/test_api.py index 4ec136d..39e9a44 100644 --- a/glance/tests/unit/v1/test_api.py +++ b/glance/tests/unit/v1/test_api.py @@ -1071,31 +1071,23 @@ class TestGlanceAPI(base.IsolatedUnitTest): def test_add_copy_from_with_restricted_sources(self): """Tests creates an image from copy-from with restricted sources""" - fixture_headers = {'x-image-meta-store': 'file', + header_template = {'x-image-meta-store': 'file', 'x-image-meta-disk-format': 'vhd', - 'x-glance-api-copy-from': 'file:///etc/passwd', 'x-image-meta-container-format': 'ovf', 'x-image-meta-name': 'fake image #F'} - req = webob.Request.blank("/images") - req.method = 'POST' - for k, v in six.iteritems(fixture_headers): - req.headers[k] = v - res = req.get_response(self.api) - self.assertEqual(400, res.status_int) + schemas = ["file:///etc/passwd", + "swift+config:///xxx", + "filesystem:///etc/passwd"] - fixture_headers = {'x-image-meta-store': 'file', - 'x-image-meta-disk-format': 'vhd', - 'x-glance-api-copy-from': 'swift+config://xxx', - 'x-image-meta-container-format': 'ovf', - 'x-image-meta-name': 'fake image #F'} - - req = webob.Request.blank("/images") - req.method = 'POST' - for k, v in six.iteritems(fixture_headers): - req.headers[k] = v - res = req.get_response(self.api) - self.assertEqual(400, res.status_int) + for schema in schemas: + req = webob.Request.blank("/images") + req.method = 'POST' + for k, v in six.iteritems(header_template): + req.headers[k] = v + req.headers['x-glance-api-copy-from'] = schema + res = req.get_response(self.api) + self.assertEqual(400, res.status_int) def test_add_copy_from_upload_image_unauthorized_with_body(self): rules = {"upload_image": '!', "modify_image": '@', -- 2.0.5