From 4cb4bc39e1d1f115d6db596e0a7e6f23db3f924e Mon Sep 17 00:00:00 2001 From: Matthew Thode Date: Tue, 22 Sep 2015 12:45:22 -0500 Subject: [PATCH] app-admin/glance: fixing CVE-2015-5251 Package-Manager: portage-2.2.20.1 --- .../files/cve-2015-5251-stable-kilo.patch | 192 ++++++++++++++++++ app-admin/glance/glance-2015.1.1-r2.ebuild | 190 +++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 app-admin/glance/files/cve-2015-5251-stable-kilo.patch create mode 100644 app-admin/glance/glance-2015.1.1-r2.ebuild diff --git a/app-admin/glance/files/cve-2015-5251-stable-kilo.patch b/app-admin/glance/files/cve-2015-5251-stable-kilo.patch new file mode 100644 index 000000000000..f86864575dc0 --- /dev/null +++ b/app-admin/glance/files/cve-2015-5251-stable-kilo.patch @@ -0,0 +1,192 @@ +From 9beca533f42ae1fc87418de0c360e19bc59b24b5 Mon Sep 17 00:00:00 2001 +From: Stuart McLaren +Date: Tue, 11 Aug 2015 10:37:09 +0000 +Subject: [PATCH] Prevent image status being directly modified via v1 + +Users shouldn't be able to change an image's status directly via the +v1 API. + +Some existing consumers of Glance set the x-image-meta-status header in +requests to the Glance API, eg: + +https://github.com/openstack/nova/blob/master/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance#L184 + +We should try to prevent users setting 'status' via v1, but without breaking +existing benign API calls such as these. + +I've adopted the following approach (which has some prior art in 'protected properties'). + +If a PUT request is received which contains an x-image-meta-status header: + +* The user provided status is ignored if it matches the current image + status (this prevents benign calls such as the nova one above from + breaking). The usual code (eg 200) will be returned. + +* If the user provided status doesn't match the current image status (ie + there is a real attempt to change the value) 403 will be returned. This + will break any calls which currently intentionally change the status. + +APIImpact + +Closes-bug: 1482371 + +Change-Id: I44fadf32abb57c962b67467091c3f51c1ccc25e6 +(cherry picked from commit 4d08db5b6d42323ac1958ef3b7417d875e7bea8c) +--- + glance/api/v1/__init__.py | 3 + + glance/api/v1/images.py | 9 +++ + glance/tests/functional/v1/test_api.py | 89 ++++++++++++++++++++++ + .../integration/legacy_functional/test_v1_api.py | 2 + + 4 files changed, 103 insertions(+) + +diff --git a/glance/api/v1/__init__.py b/glance/api/v1/__init__.py +index 74de9aa1411d8e926770b67f7d851cf14e794414..9306bbb4fe78f77a26bb539c717fdfd2b38767c8 100644 +--- a/glance/api/v1/__init__.py ++++ b/glance/api/v1/__init__.py +@@ -21,3 +21,6 @@ SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir') + + # Metadata which only an admin can change once the image is active + ACTIVE_IMMUTABLE = ('size', 'checksum') ++ ++# Metadata which cannot be changed (irrespective of the current image state) ++IMMUTABLE = ('status',) +diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py +index e33b91fbca79377e78ccfd329fa542ad422f5ffc..95e32949d958d0f57a3b60c141b91784a5801f5a 100644 +--- a/glance/api/v1/images.py ++++ b/glance/api/v1/images.py +@@ -57,6 +57,7 @@ _LW = i18n._LW + SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS + SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS + ACTIVE_IMMUTABLE = glance.api.v1.ACTIVE_IMMUTABLE ++IMMUTABLE = glance.api.v1.IMMUTABLE + + CONF = cfg.CONF + CONF.import_opt('disk_formats', 'glance.common.config', group='image_format') +@@ -912,6 +913,14 @@ class Controller(controller.BaseController): + request=req, + content_type="text/plain") + ++ for key in IMMUTABLE: ++ if (image_meta.get(key) is not None and ++ image_meta.get(key) != orig_image_meta.get(key)): ++ msg = _("Forbidden to modify '%s' of image.") % key ++ raise HTTPForbidden(explanation=msg, ++ request=req, ++ content_type="text/plain") ++ + # The default behaviour for a PUT /images/ is to + # override any properties that were previously set. This, however, + # leads to a number of issues for the common use case where a caller +diff --git a/glance/tests/functional/v1/test_api.py b/glance/tests/functional/v1/test_api.py +index 9fba3bb5e40c8742530691228c7b436b385fc2ca..6b3bfbb4270f1eb0f50418504e65be30ea23d10b 100644 +--- a/glance/tests/functional/v1/test_api.py ++++ b/glance/tests/functional/v1/test_api.py +@@ -715,3 +715,92 @@ class TestApi(functional.FunctionalTest): + self.assertEqual(404, response.status) + + self.stop_servers() ++ ++ def test_status_cannot_be_manipulated_directly(self): ++ self.cleanup() ++ self.start_servers(**self.__dict__.copy()) ++ headers = minimal_headers('Image1') ++ ++ # Create a 'queued' image ++ http = httplib2.Http() ++ headers = {'Content-Type': 'application/octet-stream', ++ 'X-Image-Meta-Disk-Format': 'raw', ++ 'X-Image-Meta-Container-Format': 'bare'} ++ path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) ++ response, content = http.request(path, 'POST', headers=headers, ++ body=None) ++ self.assertEqual(201, response.status) ++ image = jsonutils.loads(content)['image'] ++ self.assertEqual('queued', image['status']) ++ ++ # Ensure status of 'queued' image can't be changed ++ path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port, ++ image['id']) ++ http = httplib2.Http() ++ headers = {'X-Image-Meta-Status': 'active'} ++ response, content = http.request(path, 'PUT', headers=headers) ++ self.assertEqual(403, response.status) ++ response, content = http.request(path, 'HEAD') ++ self.assertEqual(200, response.status) ++ self.assertEqual('queued', response['x-image-meta-status']) ++ ++ # We allow 'setting' to the same status ++ http = httplib2.Http() ++ headers = {'X-Image-Meta-Status': 'queued'} ++ response, content = http.request(path, 'PUT', headers=headers) ++ self.assertEqual(200, response.status) ++ response, content = http.request(path, 'HEAD') ++ self.assertEqual(200, response.status) ++ self.assertEqual('queued', response['x-image-meta-status']) ++ ++ # Make image active ++ http = httplib2.Http() ++ headers = {'Content-Type': 'application/octet-stream'} ++ response, content = http.request(path, 'PUT', headers=headers, ++ body='data') ++ self.assertEqual(200, response.status) ++ image = jsonutils.loads(content)['image'] ++ self.assertEqual('active', image['status']) ++ ++ # Ensure status of 'active' image can't be changed ++ http = httplib2.Http() ++ headers = {'X-Image-Meta-Status': 'queued'} ++ response, content = http.request(path, 'PUT', headers=headers) ++ self.assertEqual(403, response.status) ++ response, content = http.request(path, 'HEAD') ++ self.assertEqual(200, response.status) ++ self.assertEqual('active', response['x-image-meta-status']) ++ ++ # We allow 'setting' to the same status ++ http = httplib2.Http() ++ headers = {'X-Image-Meta-Status': 'active'} ++ response, content = http.request(path, 'PUT', headers=headers) ++ self.assertEqual(200, response.status) ++ response, content = http.request(path, 'HEAD') ++ self.assertEqual(200, response.status) ++ self.assertEqual('active', response['x-image-meta-status']) ++ ++ # Create a 'queued' image, ensure 'status' header is ignored ++ http = httplib2.Http() ++ path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) ++ headers = {'Content-Type': 'application/octet-stream', ++ 'X-Image-Meta-Status': 'active'} ++ response, content = http.request(path, 'POST', headers=headers, ++ body=None) ++ self.assertEqual(201, response.status) ++ image = jsonutils.loads(content)['image'] ++ self.assertEqual('queued', image['status']) ++ ++ # Create an 'active' image, ensure 'status' header is ignored ++ http = httplib2.Http() ++ path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) ++ headers = {'Content-Type': 'application/octet-stream', ++ 'X-Image-Meta-Disk-Format': 'raw', ++ 'X-Image-Meta-Status': 'queued', ++ 'X-Image-Meta-Container-Format': 'bare'} ++ response, content = http.request(path, 'POST', headers=headers, ++ body='data') ++ self.assertEqual(201, response.status) ++ image = jsonutils.loads(content)['image'] ++ self.assertEqual('active', image['status']) ++ self.stop_servers() +diff --git a/glance/tests/integration/legacy_functional/test_v1_api.py b/glance/tests/integration/legacy_functional/test_v1_api.py +index dff436465919569480bdbac537d20a6d61c98f46..511d46dfe18028bb430504784cc9d24c58736c3b 100644 +--- a/glance/tests/integration/legacy_functional/test_v1_api.py ++++ b/glance/tests/integration/legacy_functional/test_v1_api.py +@@ -358,6 +358,8 @@ class TestApi(base.ApiTest): + path = "/v1/images" + response, content = self.http.request(path, 'POST', headers=headers) + self.assertEqual(201, response.status) ++ image = jsonutils.loads(content)['image'] ++ self.assertEqual('active', image['status']) + + # 2. HEAD image-location + # Verify image size is zero and the status is active +-- +2.5.0 + diff --git a/app-admin/glance/glance-2015.1.1-r2.ebuild b/app-admin/glance/glance-2015.1.1-r2.ebuild new file mode 100644 index 000000000000..2083b07375e1 --- /dev/null +++ b/app-admin/glance/glance-2015.1.1-r2.ebuild @@ -0,0 +1,190 @@ +# Copyright 1999-2015 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# $Id$ + +EAPI=5 +PYTHON_COMPAT=( python2_7 ) + +inherit distutils-r1 user + +DESCRIPTION="Provides services for discovering, registering, and retrieving +virtual machine images" +HOMEPAGE="https://launchpad.net/glance" +SRC_URI="https://launchpad.net/${PN}/kilo/${PV}/+download/${P}.tar.gz" + +LICENSE="Apache-2.0" +SLOT="0" +KEYWORDS="~amd64 ~x86" +IUSE="doc mysql postgres +sqlite +swift test" +REQUIRED_USE="|| ( mysql postgres sqlite )" + +DEPEND=" + dev-python/setuptools[${PYTHON_USEDEP}] + >=dev-python/pbr-0.8.0[${PYTHON_USEDEP}] + =dev-python/hacking-0.10.0[${PYTHON_USEDEP}] + =dev-python/coverage-3.6[${PYTHON_USEDEP}] + >=dev-python/fixtures-0.3.14[${PYTHON_USEDEP}] + =dev-python/mock-1.0[${PYTHON_USEDEP}] + =dev-python/sphinx-1.1.2[${PYTHON_USEDEP}] + !~dev-python/sphinx-1.2.0[${PYTHON_USEDEP}] + =dev-python/requests-2.2.0[${PYTHON_USEDEP}] + !~dev-python/requests-2.4.0[${PYTHON_USEDEP}] + >=dev-python/testrepository-0.0.18[${PYTHON_USEDEP}] + >=dev-python/testtools-0.9.36[${PYTHON_USEDEP}] + !~dev-python/testtools-1.2.0[${PYTHON_USEDEP}] + >=dev-python/psutil-1.1.1[${PYTHON_USEDEP}] + =dev-python/oslotest-1.5.1[${PYTHON_USEDEP}] + =dev-python/pyxattr-0.5.0[${PYTHON_USEDEP}] + >=dev-python/oslo-sphinx-2.5.0[${PYTHON_USEDEP}] + =dev-python/elasticsearch-py-1.3.0[${PYTHON_USEDEP}] + )" + +#note to self, wsgiref is a python builtin, no need to package it +#>=dev-python/wsgiref-0.1.2[${PYTHON_USEDEP}] + +RDEPEND=" + >=dev-python/greenlet-0.3.2[${PYTHON_USEDEP}] + sqlite? ( + >=dev-python/sqlalchemy-0.9.7[sqlite,${PYTHON_USEDEP}] + <=dev-python/sqlalchemy-0.9.99[sqlite,${PYTHON_USEDEP}] + ) + mysql? ( + dev-python/mysql-python + >=dev-python/sqlalchemy-0.9.7[${PYTHON_USEDEP}] + <=dev-python/sqlalchemy-0.9.99[${PYTHON_USEDEP}] + ) + postgres? ( + dev-python/psycopg:2 + >=dev-python/sqlalchemy-0.9.7[${PYTHON_USEDEP}] + <=dev-python/sqlalchemy-0.9.99[${PYTHON_USEDEP}] + ) + >=dev-python/anyjson-0.3.3[${PYTHON_USEDEP}] + >=dev-python/eventlet-0.16.1[${PYTHON_USEDEP}] + !~dev-python/eventlet-0.17.0[${PYTHON_USEDEP}] + >=dev-python/pastedeploy-1.5.0[${PYTHON_USEDEP}] + >=dev-python/routes-1.12.3[${PYTHON_USEDEP}] + !~dev-python/routes-2.0[${PYTHON_USEDEP}] + >=dev-python/webob-1.2.3[${PYTHON_USEDEP}] + >=dev-python/sqlalchemy-migrate-0.9.5[${PYTHON_USEDEP}] + >=dev-python/httplib2-0.7.5[${PYTHON_USEDEP}] + >=dev-python/kombu-2.5.0[${PYTHON_USEDEP}] + >=dev-python/pycrypto-2.6[${PYTHON_USEDEP}] + >=dev-python/iso8601-0.1.9[${PYTHON_USEDEP}] + dev-python/ordereddict[${PYTHON_USEDEP}] + >=dev-python/oslo-config-1.9.3[${PYTHON_USEDEP}] + =dev-python/oslo-concurrency-1.8.0[${PYTHON_USEDEP}] + =dev-python/oslo-context-0.2.0[${PYTHON_USEDEP}] + =dev-python/oslo-utils-1.4.0[${PYTHON_USEDEP}] + =dev-python/stevedore-1.3.0[${PYTHON_USEDEP}] + =dev-python/taskflow-0.7.1[${PYTHON_USEDEP}] + =dev-python/keystonemiddleware-1.5.0[${PYTHON_USEDEP}] + =dev-python/WSME-0.6[${PYTHON_USEDEP}] + =dev-python/python-swiftclient-2.2.0[${PYTHON_USEDEP}] + =dev-python/oslo-vmware-0.11.1[${PYTHON_USEDEP}] + =dev-python/jsonschema-2.0.0[${PYTHON_USEDEP}] + =dev-python/python-keystoneclient-1.2.0[${PYTHON_USEDEP}] + =dev-python/pyopenssl-0.11[${PYTHON_USEDEP}] + >=dev-python/six-1.9.0[${PYTHON_USEDEP}] + >=dev-python/oslo-db-1.7.0[${PYTHON_USEDEP}] + =dev-python/oslo-i18n-1.5.0[${PYTHON_USEDEP}] + =dev-python/oslo-log-1.0.0[${PYTHON_USEDEP}] + =dev-python/oslo-messaging-1.8.0[${PYTHON_USEDEP}] + =dev-python/oslo-policy-0.3.1[${PYTHON_USEDEP}] + =dev-python/oslo-serialization-1.4.0[${PYTHON_USEDEP}] + =dev-python/retrying-1.2.3[${PYTHON_USEDEP}] + !~dev-python/retrying-1.3.0[${PYTHON_USEDEP}] + >=dev-python/osprofiler-0.3.0[${PYTHON_USEDEP}] + >=dev-python/glance_store-0.3.0[${PYTHON_USEDEP}] + =dev-python/semantic_version-2.3.1[${PYTHON_USEDEP}] +" + +PATCHES=( + "${FILESDIR}/cve-2015-5163-stable-kilo.patch" + "${FILESDIR}/cve-2015-5251-stable-kilo.patch" +) + +pkg_setup() { + enewgroup glance + enewuser glance -1 -1 /var/lib/glance glance +} + +python_prepare_all() { + sed -i '/xattr/d' test-requirements.txt || die + sed -i '/pysendfile/d' test-requirements.txt || die + distutils-r1_python_prepare_all +} + +python_compile_all() { + use doc && "${PYTHON}" setup.py build_sphinx +} + +python_test() { + # https://bugs.launchpad.net/glance/+bug/1251105 + # https://bugs.launchpad.net/glance/+bug/1242501 + testr init + testr run --parallel || die "failed testsuite under python2.7" +} + +python_install() { + distutils-r1_python_install + + for svc in api registry scrubber; do + newinitd "${FILESDIR}/glance.initd" glance-${svc} + done + + diropts -m 0750 -o glance -g glance + dodir /var/log/glance /var/lib/glance/images /var/lib/glance/scrubber + keepdir /etc/glance + keepdir /var/log/glance + keepdir /var/lib/glance/images + keepdir /var/lib/glance/scrubber + + insinto /etc/glance + insopts -m 0640 -o glance -g glance + doins etc/*.ini + doins etc/*.conf + doins etc/*.sample +} + +python_install_all() { + use doc && local HTML_DOCS=( doc/build/html/. ) + distutils-r1_python_install_all +} -- 2.26.2