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.*

Reply via email to