On 07/05/2016 07:37 PM, Moritz Mühlenhoff wrote: > On Wed, Jun 29, 2016 at 03:50:47PM +0200, Thomas Goirand wrote: >> On 06/29/2016 11:24 AM, Moritz Muehlenhoff wrote: >>> Hi Thomas, >>> https://bugs.launchpad.net/bugs/1567673 has been assigned CVE-2016-4428 and >>> I think we should fix >>> it in jessie-security. Can you please prepare an update? unstable also >>> needs the patch. >>> >>> Cheers, >>> Moritz >>> >> >> Hi Moritz, >> >> I have uploaded fixes for both Sid and Experimental, and the fix for >> Stable is committed to Git in here: >> >> http://anonscm.debian.org/cgit/openstack/horizon.git/commit/?h=debian/icehouse&id=d74e751ce93f03240f3ad4206e93d6e7e05da55f >> >> Since you may prefer a diff to read from your mail client, I have >> attached it to this message. > > Why do you upload something different than the debdiff you sent? > > jessie has 2014.1.3-7, and what you uploaded includes an additional > fix which was never on security.debian.org: > >> horizon (2014.1.3-7+deb8u1) jessie-security; urgency=high >> >> * Fix CVE-2015-3219 with upstream patch (Closes: 788306). >> >> -- Thomas Goirand <z...@debian.org> Wed, 10 Jun 2015 16:18:34 +0200 > > Cheers, > Moritz
Attached the output of: git diff -u -r debian/2014.1.3-7 \ >horizon_2014.1.3-7_to_2014.1.3-7+deb8u2.diff Can you review that instead of previous diff? Cheers, Thomas Goirand (zigo)
diff --git a/MANIFEST.in b/MANIFEST.in index 5f29627..c10ab94 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ recursive-include doc *.py *.rst *.css *.js *.html *.conf *.jpg *.gif *.png *.css_t -recursive-include horizon *.html *.css *.js *.csv *.template *.tmpl *.mo *.po -recursive-include openstack_dashboard *.html *.js *.less *.mo *.po *.example *.eot *.svg *.ttf *.woff *.png *.ico *.wsgi *.gif *.csv *.template +recursive-include horizon *.html *.css *.js *.csv *.template *.tmpl *.mo *.po *.py +recursive-include openstack_dashboard *.html *.js *.less *.mo *.po *.example *.eot *.svg *.ttf *.woff *.png *.ico *.wsgi *.gif *.csv *.template *.py recursive-include tools *.py *.sh include AUTHORS diff --git a/debian/changelog b/debian/changelog index 9d004c1..276e48e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,17 @@ +horizon (2014.1.3-7+deb8u2) jessie-security; urgency=medium + + * CVE-2016-4428: Possible client side template injection in horizon. Applied + upstream patch: "Escape angularjs templating in unsafe HTML" after rebasing + it for Icehouse (Closes: #828967). + + -- Thomas Goirand <z...@debian.org> Wed, 29 Jun 2016 15:24:16 +0200 + +horizon (2014.1.3-7+deb8u1) jessie-security; urgency=high + + * Fix CVE-2015-3219 with upstream patch (Closes: 788306). + + -- Thomas Goirand <z...@debian.org> Wed, 10 Jun 2015 16:18:34 +0200 + horizon (2014.1.3-7) unstable; urgency=medium * Fix Moscow timezone check and avoid FTBFS (Closes: #775636). diff --git a/debian/patches/CVE-2015-3219_XSS_in_Horizon_Heat_stack_creation.patch b/debian/patches/CVE-2015-3219_XSS_in_Horizon_Heat_stack_creation.patch new file mode 100644 index 0000000..58a39fc --- /dev/null +++ b/debian/patches/CVE-2015-3219_XSS_in_Horizon_Heat_stack_creation.patch @@ -0,0 +1,36 @@ +Description: Escape the description param from heat template + The heat template allows user to define custom parameters, + the fields are then converted to input fields. The description + param maps to the help_text attribute of the field. + . + Since the value comes from the user, the value must be escaped + before rendering. +Origin: upstream, https://review.openstack.org/#/c/189821/ +Bug: https://bugs.launchpad.net/horizon/+bug/1453074 +Bug-Debian: https://bugs.debian.org/788306 +Forwarded: not-needed +Author: Lin Hua Cheng <os.lch...@gmail.com> +Reviewed-By: David Lyle <david.l...@intel.com> +Last-Update: 2015-06-09 + +--- + +--- horizon-2014.1.3.orig/openstack_dashboard/dashboards/project/stacks/forms.py ++++ horizon-2014.1.3/openstack_dashboard/dashboards/project/stacks/forms.py +@@ -15,6 +15,7 @@ + import json + import logging + ++from django.utils import html + from django.utils.translation import ugettext_lazy as _ + from django.views.decorators.debug import sensitive_variables # noqa + +@@ -307,7 +308,7 @@ class CreateStackForm(forms.SelfHandling + field_args = { + 'initial': param.get('Default', None), + 'label': param_key, +- 'help_text': param.get('Description', ''), ++ 'help_text': html.escape(param.get('Description', '')), + 'required': param.get('Default', None) is None + } + diff --git a/debian/patches/CVE-2016-4428_Escape_angularjs_templating_in_unsafe_HTML.patc b/debian/patches/CVE-2016-4428_Escape_angularjs_templating_in_unsafe_HTML.patc new file mode 100644 index 0000000..f626e46 --- /dev/null +++ b/debian/patches/CVE-2016-4428_Escape_angularjs_templating_in_unsafe_HTML.patc @@ -0,0 +1,74 @@ +Description: CVE-2016-4428: Escape angularjs templating in unsafe HTML + This code extends the unsafe (typically user-supplied) HTML escape + built into Django to also escape angularjs templating markers. Safe + HTML will be unaffected. +Bug-Ubuntu: https://launchpad.net/bugs/1567673 +Bug-Debian: https://bugs.debian.org/828967 +Origin: upstream, https://review.openstack.org/#/c/329997/ +Change-Id: I0cbebfd0f814bdf1bf8c06833abf33cc2d4748e7 +Author: Richard Jones <r1chardj0...@gmail.com> +Date: Tue, 3 May 2016 05:51:49 +0000 (+1000) +X-Git-Url: https://review.openstack.org/gitweb?p=openstack%2Fhorizon.git;a=commitdiff_plain;h=d585e5eb9acf92d10d39b6c2038917a7e8ac71bb +Last-Update: 2016-06-29 + +--- /dev/null ++++ horizon-2014.1.3/horizon/utils/escape.py +@@ -0,0 +1,31 @@ ++# Copyright 2016, Rackspace, US, Inc. ++# ++# Licensed under the Apache License, Version 2.0 (the "License"); ++# you may not use this file except in compliance with the License. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++ ++import django.utils.html ++ ++ ++def escape(text, existing=django.utils.html.escape): ++ # Replace our angular markup string with a different string ++ # (which just happens to be the Django comment string) ++ # this prevents user-supplied data from being intepreted in ++ # our pages by angularjs, thus preventing it from being used ++ # for XSS attacks. Note that we use {$ $} instead of the ++ # standard {{ }} - this is configured in horizon.framework ++ # angularjs module through $interpolateProvider ++ return existing(text).replace('{$', '{%').replace('$}', '%}') ++ ++ ++# this will be invoked as early as possible in settings.py ++def monkeypatch_escape(): ++ django.utils.html.escape = escape +--- horizon-2014.1.3.orig/openstack_dashboard/settings.py ++++ horizon-2014.1.3/openstack_dashboard/settings.py +@@ -27,6 +27,10 @@ from django.utils.translation import uge + + from openstack_dashboard import exceptions + ++from horizon.utils.escape import monkeypatch_escape ++ ++monkeypatch_escape() ++ + warnings.formatwarning = lambda message, category, *args, **kwargs: \ + '%s: %s' % (category.__name__, message) + +--- horizon-2014.1.3.orig/openstack_dashboard/test/settings.py ++++ horizon-2014.1.3/openstack_dashboard/test/settings.py +@@ -17,6 +17,11 @@ from horizon.utils import secret_key + + from openstack_dashboard import exceptions + ++from horizon.utils.escape import monkeypatch_escape ++ ++# this is used to protect from client XSS attacks, but it's worth ++# enabling in our test setup to find any issues it might cause ++monkeypatch_escape() + + TEST_DIR = os.path.dirname(os.path.abspath(__file__)) + ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, "..")) diff --git a/debian/patches/series b/debian/patches/series index 7a2d683..3c10d66 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -8,3 +8,5 @@ disable-failed-django-1.7-test.patch Update_WSGI_app_creation_to_be_compatible_with_Django_1.7.patch CVE-2014-8124_Horizon_login_page_contains_DOS_attack_mechanism_icehouse_.patch fix-moscow-tz-test.patch +CVE-2015-3219_XSS_in_Horizon_Heat_stack_creation.patch +CVE-2016-4428_Escape_angularjs_templating_in_unsafe_HTML.patc diff --git a/horizon/middleware.py b/horizon/middleware.py index e4b72b2..3cdb36e 100644 --- a/horizon/middleware.py +++ b/horizon/middleware.py @@ -49,6 +49,17 @@ class HorizonMiddleware(object): def process_request(self, request): """Adds data necessary for Horizon to function to the request.""" + + request.horizon = {'dashboard': None, + 'panel': None, + 'async_messages': []} + if not hasattr(request, "user") or not request.user.is_authenticated(): + # proceed no further if the current request is already known + # not to be authenticated + # it is CRITICAL to perform this check as early as possible + # to avoid creating too many sessions + return None + # Activate timezone handling tz = request.session.get('django_timezone') if tz: @@ -62,14 +73,6 @@ class HorizonMiddleware(object): last_activity = request.session.get('last_activity', None) timestamp = int(time.time()) - request.horizon = {'dashboard': None, - 'panel': None, - 'async_messages': []} - - if not hasattr(request, "user") or not request.user.is_authenticated(): - # proceed no further if the current request is already known - # not to be authenticated - return None # If we use cookie-based sessions, check that the cookie size does not # reach the max size accepted by common web browsers. diff --git a/horizon/tables/base.py b/horizon/tables/base.py index 4aceb81..9ec5985 100644 --- a/horizon/tables/base.py +++ b/horizon/tables/base.py @@ -401,12 +401,14 @@ class Column(html.HTMLElement): data = filter(lambda datum: datum is not None, data) if len(data): - summation = summation_function(data) - for filter_func in self.filters: - summation = filter_func(summation) - return summation - else: - return None + try: + summation = summation_function(data) + for filter_func in self.filters: + summation = filter_func(summation) + return summation + except TypeError: + pass + return None class Row(html.HTMLElement): diff --git a/horizon/test/tests/tables.py b/horizon/test/tests/tables.py index e0f59ee..e605c8e 100644 --- a/horizon/test/tests/tables.py +++ b/horizon/test/tests/tables.py @@ -999,6 +999,15 @@ class DataTableTests(test.TestCase): self.assertNotContains(res, '<td>3.0</td>') self.assertNotContains(res, '<td>6</td>') + # Even if "average" summation method is specified, + # we have summation fields but no value is provoded + # if the provided data cannot be summed. + table = MyTable(self.request, TEST_DATA) + res = http.HttpResponse(table.render()) + self.assertContains(res, '<tr class="summation"') + self.assertNotContains(res, '<td>3.0</td>') + self.assertNotContains(res, '<td>6</td>') + def test_table_action_attributes(self): table = MyTable(self.request, TEST_DATA) self.assertTrue(table.has_actions) diff --git a/manage.py b/manage.py index 5818a6d..4fbb9e3 100755 --- a/manage.py +++ b/manage.py @@ -17,6 +17,8 @@ import sys from django.core.management import execute_from_command_line # noqa +sys.path.append("/usr/share/openstack-dashboard") + if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openstack_dashboard.settings") diff --git a/openstack_dashboard/dashboards/admin/projects/tests.py b/openstack_dashboard/dashboards/admin/projects/tests.py index 1cd5630..65a1b6f 100644 --- a/openstack_dashboard/dashboards/admin/projects/tests.py +++ b/openstack_dashboard/dashboards/admin/projects/tests.py @@ -1423,42 +1423,6 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): self.assertMessageCount(error=1, warning=0) self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api.keystone: ('get_default_role', - 'tenant_get', - 'domain_get'), - quotas: ('get_tenant_quota_data', - 'get_disabled_quotas')}) - def test_update_project_when_default_role_does_not_exist(self): - project = self.tenants.first() - domain_id = project.domain_id - quota = self.quotas.first() - - api.keystone.get_default_role(IsA(http.HttpRequest)) \ - .MultipleTimes().AndReturn(None) # Default role doesn't exist - api.keystone.tenant_get(IsA(http.HttpRequest), self.tenant.id, - admin=True) \ - .AndReturn(project) - api.keystone.domain_get(IsA(http.HttpRequest), domain_id) \ - .AndReturn(self.domain) - quotas.get_disabled_quotas(IsA(http.HttpRequest)) \ - .AndReturn(self.disabled_quotas.first()) - quotas.get_tenant_quota_data(IsA(http.HttpRequest), - tenant_id=self.tenant.id) \ - .AndReturn(quota) - self.mox.ReplayAll() - - url = reverse('horizon:admin:projects:update', - args=[self.tenant.id]) - - try: - # Avoid the log message in the test output when the workflow's - # step action cannot be instantiated - logging.disable(logging.ERROR) - with self.assertRaises(exceptions.NotFound): - self.client.get(url) - finally: - logging.disable(logging.NOTSET) - class UsageViewTests(test.BaseAdminViewTests): def _stub_nova_api_calls(self, nova_stu_enabled=True): diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index e3e7f23..bb2cfc4 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -627,7 +627,7 @@ class InstanceTests(test.TestCase): api.nova.flavor_get(IsA(http.HttpRequest), server.flavor['id']) \ .AndReturn(self.flavors.first()) api.network.server_security_groups(IsA(http.HttpRequest), server.id) \ - .AndReturn(self.security_groups.first()) + .AndReturn(self.security_groups.list()) self.mox.ReplayAll() @@ -654,7 +654,7 @@ class InstanceTests(test.TestCase): api.nova.flavor_get(IsA(http.HttpRequest), server.flavor['id']) \ .AndReturn(self.flavors.first()) api.network.server_security_groups(IsA(http.HttpRequest), server.id) \ - .AndReturn(self.security_groups.first()) + .AndReturn(self.security_groups.list()) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/project/stacks/forms.py b/openstack_dashboard/dashboards/project/stacks/forms.py index b7c6d1f..8c4143a 100644 --- a/openstack_dashboard/dashboards/project/stacks/forms.py +++ b/openstack_dashboard/dashboards/project/stacks/forms.py @@ -15,6 +15,7 @@ import json import logging +from django.utils import html from django.utils.translation import ugettext_lazy as _ from django.views.decorators.debug import sensitive_variables # noqa @@ -307,7 +308,7 @@ class CreateStackForm(forms.SelfHandlingForm): field_args = { 'initial': param.get('Default', None), 'label': param_key, - 'help_text': param.get('Description', ''), + 'help_text': html.escape(param.get('Description', '')), 'required': param.get('Default', None) is None } diff --git a/openstack_dashboard/dashboards/settings/user/tests.py b/openstack_dashboard/dashboards/settings/user/tests.py index 2003f1b..81b2356 100644 --- a/openstack_dashboard/dashboards/settings/user/tests.py +++ b/openstack_dashboard/dashboards/settings/user/tests.py @@ -29,7 +29,7 @@ class UserSettingsTest(test.TestCase): res = self.client.get(INDEX_URL) self.assertContains(res, "Australia/Melbourne (UTC +11:00)") - self.assertContains(res, "Europe/Moscow (UTC +04:00)") + self.assertContains(res, "Europe/Moscow (UTC +0") self.assertContains(res, "Atlantic/Stanley (UTC -03:00)") self.assertContains(res, "Pacific/Honolulu (UTC -10:00)") diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py index 79f7b22..0338af6 100644 --- a/openstack_dashboard/settings.py +++ b/openstack_dashboard/settings.py @@ -27,6 +27,10 @@ from django.utils.translation import ugettext_lazy as _ from openstack_dashboard import exceptions +from horizon.utils.escape import monkeypatch_escape + +monkeypatch_escape() + warnings.formatwarning = lambda message, category, *args, **kwargs: \ '%s: %s' % (category.__name__, message) diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index 7cf4b8f..b753109 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -17,6 +17,11 @@ from horizon.utils import secret_key from openstack_dashboard import exceptions +from horizon.utils.escape import monkeypatch_escape + +# this is used to protect from client XSS attacks, but it's worth +# enabling in our test setup to find any issues it might cause +monkeypatch_escape() TEST_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, "..")) diff --git a/openstack_dashboard/views.py b/openstack_dashboard/views.py index 8a630e9..5ff1fd5 100644 --- a/openstack_dashboard/views.py +++ b/openstack_dashboard/views.py @@ -33,6 +33,4 @@ def splash(request): if request.user.is_authenticated(): return shortcuts.redirect(horizon.get_user_home(request.user)) form = forms.Login(request) - request.session.clear() - request.session.set_test_cookie() return shortcuts.render(request, 'splash.html', {'form': form}) diff --git a/openstack_dashboard/wsgi/django.wsgi b/openstack_dashboard/wsgi/django.wsgi index 1e92a4d..05c8631 100644 --- a/openstack_dashboard/wsgi/django.wsgi +++ b/openstack_dashboard/wsgi/django.wsgi @@ -1,7 +1,7 @@ import logging import os import sys -import django.core.handlers.wsgi +from django.core.wsgi import get_wsgi_application from django.conf import settings # Add this file path to sys.path in order to import settings @@ -9,7 +9,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), '.. os.environ['DJANGO_SETTINGS_MODULE'] = 'openstack_dashboard.settings' sys.stdout = sys.stderr -DEBUG = False +sys.path.append("/usr/share/openstack-dashboard/") -application = django.core.handlers.wsgi.WSGIHandler() +DEBUG = False +application = get_wsgi_application() diff --git a/run_tests.sh b/run_tests.sh index ffab99e..e691d2e 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -319,8 +319,8 @@ function run_tests_all { export NOSE_HTML_OUT_FILE='horizon_nose_results.html' fi if [ $with_coverage -eq 1 ]; then - ${command_wrapper} coverage erase - coverage_run="coverage run -p" + ${command_wrapper} python -m coverage erase + coverage_run="python -m coverage run -p" fi ${command_wrapper} ${coverage_run} $root/manage.py test horizon --settings=horizon.test.settings $testopts # get results of the Horizon tests @@ -337,9 +337,9 @@ function run_tests_all { if [ $with_coverage -eq 1 ]; then echo "Generating coverage reports" - ${command_wrapper} coverage combine - ${command_wrapper} coverage xml -i --include="horizon/*,openstack_dashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' - ${command_wrapper} coverage html -i --include="horizon/*,openstack_dashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' -d reports + ${command_wrapper} python -m coverage combine + ${command_wrapper} python -m coverage xml -i --include="horizon/*,openstack_dashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' + ${command_wrapper} python -m coverage html -i --include="horizon/*,openstack_dashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' -d reports fi # Remove the leftover coverage files from the -p flag earlier. rm -f .coverage.*