app-admin/glance: fixing CVE-2015-5251
authorMatthew Thode <mthode@mthode.org>
Tue, 22 Sep 2015 17:45:22 +0000 (12:45 -0500)
committerMatthew Thode <mthode@mthode.org>
Tue, 22 Sep 2015 17:45:59 +0000 (12:45 -0500)
Package-Manager: portage-2.2.20.1

app-admin/glance/files/cve-2015-5251-stable-kilo.patch [new file with mode: 0644]
app-admin/glance/glance-2015.1.1-r2.ebuild [new file with mode: 0644]

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 (file)
index 0000000..f868645
--- /dev/null
@@ -0,0 +1,192 @@
+From 9beca533f42ae1fc87418de0c360e19bc59b24b5 Mon Sep 17 00:00:00 2001
+From: Stuart McLaren <stuart.mclaren@hp.com>
+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/<IMAGE_ID> 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 (file)
index 0000000..2083b07
--- /dev/null
@@ -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/pbr-1.0[${PYTHON_USEDEP}]
+               test? (
+                       ${RDEPEND}
+                       >=dev-python/hacking-0.10.0[${PYTHON_USEDEP}]
+                       <dev-python/hacking-0.11[${PYTHON_USEDEP}]
+                       ~dev-python/Babel-1.3[${PYTHON_USEDEP}]
+                       >=dev-python/coverage-3.6[${PYTHON_USEDEP}]
+                       >=dev-python/fixtures-0.3.14[${PYTHON_USEDEP}]
+                       <dev-python/fixtures-1.3.0[${PYTHON_USEDEP}]
+                       >=dev-python/mock-1.0[${PYTHON_USEDEP}]
+                       <dev-python/mock-1.1.0[${PYTHON_USEDEP}]
+                       >=dev-python/sphinx-1.1.2[${PYTHON_USEDEP}]
+                       !~dev-python/sphinx-1.2.0[${PYTHON_USEDEP}]
+                       <dev-python/sphinx-1.3[${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/psutil-2.0.0[${PYTHON_USEDEP}]
+                       >=dev-python/oslotest-1.5.1[${PYTHON_USEDEP}]
+                       <dev-python/oslotest-1.6.0[${PYTHON_USEDEP}]
+                       dev-python/mysql-python[${PYTHON_USEDEP}]
+                       dev-python/psycopg[${PYTHON_USEDEP}]
+                       ~dev-python/pysendfile-2.0.1[${PYTHON_USEDEP}]
+                       dev-python/qpid-python[${PYTHON_USEDEP}]
+                       >=dev-python/pyxattr-0.5.0[${PYTHON_USEDEP}]
+                       >=dev-python/oslo-sphinx-2.5.0[${PYTHON_USEDEP}]
+                       <dev-python/oslo-sphinx-2.6.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-config-1.10.0[${PYTHON_USEDEP}]
+       >=dev-python/oslo-concurrency-1.8.0[${PYTHON_USEDEP}]
+       <dev-python/oslo-concurrency-1.9.0[${PYTHON_USEDEP}]
+       >=dev-python/oslo-context-0.2.0[${PYTHON_USEDEP}]
+       <dev-python/oslo-context-0.3.0[${PYTHON_USEDEP}]
+       >=dev-python/oslo-utils-1.4.0[${PYTHON_USEDEP}]
+       <dev-python/oslo-utils-1.5.0[${PYTHON_USEDEP}]
+       >=dev-python/stevedore-1.3.0[${PYTHON_USEDEP}]
+       <dev-python/stevedore-1.4.0[${PYTHON_USEDEP}]
+       >=dev-python/taskflow-0.7.1[${PYTHON_USEDEP}]
+       <dev-python/taskflow-0.8.0[${PYTHON_USEDEP}]
+       >=dev-python/keystonemiddleware-1.5.0[${PYTHON_USEDEP}]
+       <dev-python/keystonemiddleware-1.6.0[${PYTHON_USEDEP}]
+       >=dev-python/WSME-0.6[${PYTHON_USEDEP}]
+       <dev-python/WSME-0.7[${PYTHON_USEDEP}]
+       dev-python/posix_ipc[${PYTHON_USEDEP}]
+       swift? (
+               >=dev-python/python-swiftclient-2.2.0[${PYTHON_USEDEP}]
+               <dev-python/python-swiftclient-2.5.0[${PYTHON_USEDEP}]
+       )
+       >=dev-python/oslo-vmware-0.11.1[${PYTHON_USEDEP}]
+       <dev-python/oslo-vmware-0.12.0[${PYTHON_USEDEP}]
+       dev-python/paste[${PYTHON_USEDEP}]
+       >=dev-python/jsonschema-2.0.0[${PYTHON_USEDEP}]
+       <dev-python/jsonschema-3.0.0[${PYTHON_USEDEP}]
+       >=dev-python/python-keystoneclient-1.2.0[${PYTHON_USEDEP}]
+       <dev-python/python-keystoneclient-1.4.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-db-1.8.0[${PYTHON_USEDEP}]
+       >=dev-python/oslo-i18n-1.5.0[${PYTHON_USEDEP}]
+       <dev-python/oslo-i18n-1.6.0[${PYTHON_USEDEP}]
+       >=dev-python/oslo-log-1.0.0[${PYTHON_USEDEP}]
+       <dev-python/oslo-log-1.1.0[${PYTHON_USEDEP}]
+       >=dev-python/oslo-messaging-1.8.0[${PYTHON_USEDEP}]
+       <dev-python/oslo-messaging-1.9.0[${PYTHON_USEDEP}]
+       >=dev-python/oslo-policy-0.3.1[${PYTHON_USEDEP}]
+       <dev-python/oslo-policy-0.4.0[${PYTHON_USEDEP}]
+       >=dev-python/oslo-serialization-1.4.0[${PYTHON_USEDEP}]
+       <dev-python/oslo-serialization-1.5.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/glance_store-0.5.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
+}