浏览代码

HUE-1085 [oozie] Invalid js escaping

For dumping json safe for js, use either:
desktop.lib.django_util.encode_json_for_js
render_json(designs, js_safe=True)

Including JSONEncoderForHTML in desktop json_utils.py
Romain Rigaux 12 年之前
父节点
当前提交
d9ccf6b

+ 0 - 5
apps/jobsub/src/jobsub/tests.py

@@ -15,7 +15,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import copy
 import logging
 import time
 
@@ -24,18 +23,14 @@ try:
 except ImportError:
   import simplejson as json
 
-from nose.plugins.skip import SkipTest
 from nose.tools import assert_true, assert_false, assert_equal, assert_raises
 from django.contrib.auth.models import User
 from django.core.urlresolvers import reverse
 
 from desktop.lib.django_test_util import make_logged_in_client
-from desktop.lib.test_utils import grant_access
 from liboozie.oozie_api_test import OozieServerProvider
 from oozie.models import Workflow, Node, Action, Start, Kill, End, Link
 
-from django.template.defaultfilters import escapejs
-
 
 LOG = logging.getLogger(__name__)
 

+ 7 - 16
apps/jobsub/src/jobsub/views.py

@@ -33,29 +33,24 @@ import time as py_time
 
 from django.core import urlresolvers
 from django.shortcuts import redirect
-from django.template.defaultfilters import escapejs
 from django.utils.translation import ugettext as _
 
-from desktop.lib.django_util import render, render_json, extract_field_data
+from desktop.lib.django_util import render, render_json
 from desktop.lib.exceptions import StructuredException
 from desktop.lib.exceptions_renderable import PopupException
-from desktop.lib.rest.http_client import RestException
 from desktop.log.access import access_warn
 
-from hadoop.fs.exceptions import WebHdfsException
-from liboozie.oozie_api import get_oozie
-
 from oozie.models import Workflow
 from oozie.forms import design_form_by_type
-from oozie.utils import model_to_dict, format_dict_field_values, format_field_value,\
-                        sanitize_node_dict, JSON_FIELDS
+from oozie.utils import model_to_dict, format_dict_field_values,\
+                        sanitize_node_dict
 
+# To re-enable
 from jobsub.management.commands import jobsub_setup
 
 
 LOG = logging.getLogger(__name__)
 
-SKIP_ESCAPE = ('name', 'owner')
 
 def list_designs(request):
   '''
@@ -80,7 +75,7 @@ def list_designs(request):
           'owner': design.owner.username,
           # Design name is validated by workflow and node forms.
           'name': design.name,
-          'description': escapejs(design.description),
+          'description': design.description,
           'node_type': design.start.get_child('to').node_type,
           'last_modified': py_time.mktime(design.last_modified.timetuple()),
           'editable': design.owner.id == request.user.id
@@ -88,7 +83,7 @@ def list_designs(request):
       designs.append(ko_design)
 
   if request.is_ajax():
-    return render_json(designs)
+    return render_json(designs, js_safe=True)
   else:
     return render("designs.mako", request, {
       'currentuser': request.user,
@@ -134,12 +129,8 @@ def get_design(request, design_id):
   node = workflow.start.get_child('to')
   node_dict = model_to_dict(node)
   node_dict['id'] = design_id
-  for key in node_dict:
-    if key not in JSON_FIELDS:
-      if key not in SKIP_ESCAPE:
-        node_dict[key] = escapejs(node_dict[key])
   node_dict['editable'] = workflow.owner.id == request.user.id
-  return render_json(node_dict);
+  return render_json(node_dict, js_safe=True);
 
 
 def save_design(request, design_id):

+ 2 - 5
apps/oozie/src/oozie/models.py

@@ -32,12 +32,12 @@ from django.core.urlresolvers import reverse
 from django.core.validators import RegexValidator
 from django.contrib.auth.models import User
 from django.forms.models import inlineformset_factory
-from django.template.defaultfilters import escapejs
 from django.utils.translation import ugettext as _, ugettext_lazy as _t
 
 from desktop.log.access import access_warn
 from desktop.lib import django_mako
 from desktop.lib.exceptions_renderable import PopupException
+from desktop.lib.json_utils import JSONEncoderForHTML
 from hadoop.fs.exceptions import WebHdfsException
 
 from hadoop.fs.hadoopfs import Hdfs
@@ -158,10 +158,7 @@ class Job(models.Model):
     return self._escapejs_parameters_list(self.parameters)
 
   def _escapejs_parameters_list(self, parameters):
-    escaped = []
-    for item in json.loads(parameters):
-      escaped.append({"name": escapejs(item["name"]), "value": escapejs(item["value"])})
-    return json.dumps(escaped)
+    return json.dumps(json.loads(parameters), cls=JSONEncoderForHTML)
 
   @property
   def status(self):

+ 0 - 1
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -17,7 +17,6 @@
 <%!
   from desktop.views import commonheader, commonfooter
   from django.utils.translation import ugettext as _
-  from django.template.defaultfilters import escapejs
 %>
 
 <%namespace name="layout" file="../navigation-bar.mako" />

+ 2 - 2
apps/oozie/src/oozie/tests.py

@@ -1236,8 +1236,8 @@ class TestEditor(OozieMockBase):
 
 
   def test_xss_escape_js(self):
-    escaped = '[{"name": "oozie.use.system.libpath", "value": "true"}, {"name": "123\\\\u0022\\\\u003E\\\\u003Cscript\\\\u003Ealert(1)\\\\u003C/script\\\\u003E", "value": "hacked"}]'
-    hacked = '[{"name":"oozie.use.system.libpath","value":"true"}, {"name": "123\\"><script>alert(1)</script>", "value": "hacked"}]'
+    hacked = '[{"name":"oozie.use.system.libpath","value":"true"}, {"name": "123\\"><script>alert(1)</script>", "value": "\'hacked\'"}]'
+    escaped = '[{"name": "oozie.use.system.libpath", "value": "true"}, {"name": "123\\"\\u003e\\u003cscript\\u003ealert(1)\\u003c/script\\u003e", "value": "\'hacked\'"}]'
 
     self.wf.job_properties = hacked
     self.wf.parameters = hacked

+ 13 - 15
apps/oozie/src/oozie/views/dashboard.py

@@ -29,9 +29,10 @@ from django.utils.translation import ugettext as _
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 
-from desktop.lib.django_util import render
+from desktop.lib.django_util import render, encode_json_for_js
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.rest.http_client import RestException
+from desktop.lib.view_util import format_duration_in_millis
 from desktop.log.access import access_warn
 from liboozie.oozie_api import get_oozie
 from liboozie.submittion import Submission
@@ -42,9 +43,6 @@ from oozie.forms import RerunForm, ParameterForm, RerunCoordForm,\
 from oozie.models import History, Job, Workflow, utc_datetime_format
 from oozie.settings import DJANGO_APPS
 
-from django.template.defaultfilters import escapejs
-from desktop.lib.view_util import format_duration_in_millis
-
 
 LOG = logging.getLogger(__name__)
 
@@ -108,7 +106,7 @@ def list_oozie_workflows(request):
       json_jobs = split_oozie_jobs(workflows.jobs)['running_jobs']
     if request.GET.get('type') == 'completed':
       json_jobs = split_oozie_jobs(workflows.jobs)['completed_jobs']
-    return HttpResponse(json.dumps(massaged_oozie_jobs_for_json(json_jobs, request.user)).replace('\\\\', '\\'), mimetype="application/json")
+    return HttpResponse(encode_json_for_js(massaged_oozie_jobs_for_json(json_jobs, request.user)), mimetype="application/json")
 
   return render('dashboard/list_oozie_workflows.mako', request, {
     'user': request.user,
@@ -205,7 +203,7 @@ def list_oozie_workflow(request, job_id, coordinator_job_id=None, bundle_job_id=
       'log': oozie_workflow.log,
       'actions': massaged_workflow_actions_for_json(oozie_workflow.get_working_actions(), oozie_coordinator, oozie_bundle)
     }
-    return HttpResponse(json.dumps(return_obj).replace('\\\\', '\\'), mimetype="application/json")
+    return HttpResponse(encode_json_for_js(return_obj), mimetype="application/json")
 
   return render('dashboard/list_oozie_workflow.mako', request, {
     'history': history,
@@ -245,7 +243,7 @@ def list_oozie_coordinator(request, job_id, bundle_job_id=None):
       'log': oozie_coordinator.log,
       'actions': massaged_coordinator_actions_for_json(oozie_coordinator, oozie_bundle)
     }
-    return HttpResponse(json.dumps(return_obj).replace('\\\\', '\\'), mimetype="application/json")
+    return HttpResponse(encode_json_for_js(return_obj), mimetype="application/json")
 
   return render('dashboard/list_oozie_coordinator.mako', request, {
     'oozie_coordinator': oozie_coordinator,
@@ -507,7 +505,7 @@ def massaged_workflow_actions_for_json(workflow_actions, oozie_coordinator, oozi
       'id': action.id,
       'log': action.externalId and reverse('jobbrowser.views.job_single_logs', kwargs={'job': action.externalId}) or '',
       'url': action.get_absolute_url(),
-      'name': escapejs(action.name),
+      'name': action.name,
       'type': action.type,
       'status': action.status,
       'externalIdUrl': action.externalId and reverse('jobbrowser.views.single_job', kwargs={'job': action.externalId}) or '',
@@ -515,10 +513,10 @@ def massaged_workflow_actions_for_json(workflow_actions, oozie_coordinator, oozi
       'startTime': format_time(action.startTime),
       'endTime': format_time(action.endTime),
       'retries': action.retries,
-      'errorCode': escapejs(action.errorCode),
-      'errorMessage': escapejs(action.errorMessage),
+      'errorCode': action.errorCode,
+      'errorMessage': action.errorMessage,
       'transition': action.transition,
-      'data': escapejs(action.data),
+      'data': action.data,
     }
     actions.append(massaged_action)
 
@@ -547,9 +545,9 @@ def massaged_coordinator_actions_for_json(coordinator, oozie_bundle):
       'title': action.title,
       'createdTime': format_time(action.createdTime),
       'lastModifiedTime': format_time(action.lastModifiedTime),
-      'errorCode': escapejs(action.errorCode),
-      'errorMessage': escapejs(action.errorMessage),
-      'missingDependencies': escapejs(action.missingDependencies)
+      'errorCode': action.errorCode,
+      'errorMessage': action.errorMessage,
+      'missingDependencies': action.missingDependencies
     }
 
     actions.insert(0, massaged_action)
@@ -617,7 +615,7 @@ def massaged_oozie_jobs_for_json(oozie_jobs, user):
       'status': job.status,
       'isRunning': job.is_running(),
       'duration': job.endTime and job.startTime and format_duration_in_millis(( time.mktime(job.endTime) - time.mktime(job.startTime) ) * 1000) or None,
-      'appName': escapejs(job.appName),
+      'appName': job.appName,
       'progress': job.get_progress(),
       'user': job.user,
       'absoluteUrl': job.get_absolute_url(),

+ 16 - 3
desktop/core/src/desktop/lib/django_util.py

@@ -34,9 +34,10 @@ from django.template.loader import render_to_string as django_render_to_string
 from django.template import RequestContext
 from django.db import models
 
-from desktop.lib import django_mako
 import desktop.conf
 import desktop.lib.thrift_util
+from desktop.lib import django_mako
+from desktop.lib.json_utils import JSONEncoderForHTML
 
 # Values for template_lib parameter
 DJANGO = 'django'
@@ -259,12 +260,21 @@ def encode_json(data, indent=None):
   """
   return simplejson.dumps(data, indent=indent, cls=Encoder)
 
+def encode_json_for_js(data, indent=None):
+  """
+  Converts data into a JSON string.
+
+  Typically this is used from render_json, but it's the natural
+  endpoint to test the Encoder logic, so it's separated out.
+  """
+  return simplejson.dumps(data, indent=indent, cls=JSONEncoderForHTML)
+
 VALID_JSON_IDENTIFIER = re.compile("^[a-zA-Z_$][a-zA-Z0-9_$]*$")
 
 class IllegalJsonpCallbackNameException(Exception):
   pass
 
-def render_json(data, jsonp_callback=None):
+def render_json(data, jsonp_callback=None, js_safe=False):
   """
   Renders data as json.  If jsonp is specified, wraps
   the result in a function.
@@ -273,7 +283,10 @@ def render_json(data, jsonp_callback=None):
     indent = 2
   else:
     indent = 0
-  json = encode_json(data, indent)
+  if js_safe:
+    json = encode_json_for_js(data, indent)
+  else:
+    json = encode_json(data, indent)
   if jsonp_callback is not None:
     if not VALID_JSON_IDENTIFIER.match(jsonp_callback):
       raise IllegalJsonpCallbackNameException("Invalid jsonp callback name: %s" % jsonp_callback)

+ 51 - 0
desktop/core/src/desktop/lib/json_utils.py

@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you 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.
+
+try:
+  import json
+except ImportError:
+  import simplejson as json
+
+
+##
+## Straight copy from simplejson 2.1.0 as we are with 2.0.9 and Python 2.4
+##
+class JSONEncoderForHTML(json.JSONEncoder):
+    """An encoder that produces JSON safe to embed in HTML.
+
+    To embed JSON content in, say, a script tag on a web page, the
+    characters &, < and > should be escaped. They cannot be escaped
+    with the usual entities (e.g. &amp;) because they are not expanded
+    within <script> tags.
+    """
+
+    def encode(self, o):
+        # Override JSONEncoder.encode because it has hacks for
+        # performance that make things more complicated.
+        chunks = self.iterencode(o, True)
+        if self.ensure_ascii:
+            return ''.join(chunks)
+        else:
+            return u''.join(chunks)
+
+    def iterencode(self, o, _one_shot=False):
+        chunks = super(JSONEncoderForHTML, self).iterencode(o, _one_shot)
+        for chunk in chunks:
+            chunk = chunk.replace('&', '\\u0026')
+            chunk = chunk.replace('<', '\\u003c')
+            chunk = chunk.replace('>', '\\u003e')
+            yield chunk