Browse Source

[security] Unsafe Inline Script (#3876)

This removes unsafe-inline scripts
Tabraiz 1 year ago
parent
commit
ff1e56e88f

+ 3 - 0
desktop/conf.dist/hue.ini

@@ -137,6 +137,9 @@ http_500_debug_mode=false
 # X-Content-Type-Options: nosniff This is a HTTP response header feature that helps prevent attacks based on MIME-type confusion.
 ## secure_content_security_policy="script-src 'self' 'unsafe-inline' 'unsafe-eval' *.googletagmanager.com *.doubleclick.net data:;img-src 'self' *.doubleclick.net http://*.tile.osm.org *.tile.osm.org *.gstatic.com data:;style-src 'self' 'unsafe-inline' fonts.googleapis.com;connect-src 'self' *.google-analytics.com;frame-src *;child-src 'self' data: *.vimeo.com;object-src 'none'"
 
+# Enable nonce attribute to remove unsafe-inline and auto remove unsafe-inline from csp 
+## csp_nonce=true
+
 # Strict-Transport-Security HTTP Strict Transport Security(HSTS) is a policy which is communicated by the server to the user agent via HTTP response header field name "Strict-Transport-Security". HSTS policy specifies a period of time during which the user agent(browser) should only access the server in a secure fashion(https).
 ## secure_ssl_redirect=False
 ## secure_redirect_host=0.0.0.0

+ 4 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -142,6 +142,10 @@
   # X-Content-Type-Options: nosniff This is a HTTP response header feature that helps prevent attacks based on MIME-type confusion.
   ## secure_content_security_policy="script-src 'self' 'unsafe-inline' 'unsafe-eval' *.googletagmanager.com *.doubleclick.net data:;img-src 'self' *.doubleclick.net http://*.tile.osm.org *.tile.osm.org *.gstatic.com data:;style-src 'self' 'unsafe-inline' fonts.googleapis.com;connect-src 'self' *.google-analytics.com;frame-src *;child-src 'self' data: *.vimeo.com;object-src 'none'"
 
+  # Enable nonce attribute to remove unsafe-inline and auto remove unsafe-inline from csp 
+  ## csp_nonce=true
+
+
   # Strict-Transport-Security HTTP Strict Transport Security(HSTS) is a policy which is communicated by the server to the user agent via HTTP response header field name "Strict-Transport-Security". HSTS policy specifies a period of time during which the user agent(browser) should only access the server in a secure fashion(https).
   ## secure_ssl_redirect=False
   ## secure_redirect_host=0.0.0.0

+ 7 - 0
desktop/core/src/desktop/conf.py

@@ -387,6 +387,13 @@ SECURE_CONTENT_SECURITY_POLICY = Config(
           "child-src 'self' data: *.vimeo.com;" +
           "object-src 'none'")
 
+CSP_NONCE = Config(
+  key="csp_nonce",
+  help=_('Generates a unique nonce for each request to strengthen CSP by disallowing '
+        '‘unsafe-inline’ scripts and styles.'),
+  type=coerce_bool,
+  default=False)
+
 SECURE_SSL_REDIRECT = Config(
   key="secure_ssl_redirect",
   help=_('If all non-SSL requests should be permanently redirected to SSL.'),

+ 61 - 44
desktop/core/src/desktop/js/onePageViewModel.js

@@ -182,14 +182,12 @@ class OnePageViewModel {
       $.ajax({
         url: scriptUrl,
         converters: {
-          'text script': function (text) {
-            return text;
-          }
+          'text script': text => text
         }
       })
         .done(contents => {
           loadedJs.push(scriptUrl);
-          deferred.resolve({ url: scriptUrl, contents: contents });
+          deferred.resolve({ url: scriptUrl, contents });
         })
         .fail(() => {
           deferred.resolve('');
@@ -201,21 +199,20 @@ class OnePageViewModel {
       const promises = [];
       while (scriptUrls.length) {
         const scriptUrl = scriptUrls.shift();
-        if (loadedJs.indexOf(scriptUrl) !== -1) {
-          continue;
+        if (!loadedJs.includes(scriptUrl)) {
+          promises.push(loadScript(scriptUrl));
         }
-        promises.push(loadScript(scriptUrl));
       }
       return promises;
     };
 
     const addGlobalCss = function ($el) {
       const cssFile = $el.attr('href').split('?')[0];
-      if (loadedCss.indexOf(cssFile) === -1) {
+      if (!loadedCss.includes(cssFile)) {
         loadedCss.push(cssFile);
         $.ajaxSetup({ cache: true });
         if (window.DEV) {
-          $el.attr('href', $el.attr('href') + '?dev=' + Math.random());
+          $el.attr('href', `${$el.attr('href')}?dev=${Math.random()}`);
         }
         $el.clone().appendTo($('head'));
         $.ajaxSetup({ cache: false });
@@ -223,7 +220,7 @@ class OnePageViewModel {
       $el.remove();
     };
 
-    // Only load CSS and JS files that are not loaded before
+    // Process headers to load CSS and JS files
     self.processHeaders = function (response) {
       const promise = $.Deferred();
       const $rawHtml = $('<span>').html(response);
@@ -237,36 +234,39 @@ class OnePageViewModel {
       $allScripts.remove();
 
       $rawHtml.find('link[href]').each(function () {
-        addGlobalCss($(this)); // Also removes the elements;
+        addGlobalCss($(this));
       });
 
       $rawHtml.find('a[href]').each(function () {
         let link = $(this).attr('href');
         if (link.startsWith('/') && !link.startsWith('/hue')) {
-          link = window.HUE_BASE_URL + '/hue' + link;
+          link = `${window.HUE_BASE_URL}/hue${link}`;
         }
         $(this).attr('href', link);
       });
 
       const scriptPromises = loadScripts(scriptsToLoad);
 
-      const evalScriptSync = function () {
+      const loadScriptSync = function () {
         if (scriptPromises.length) {
-          // Evaluate the scripts in the order they were defined in the page
           const nextScriptPromise = scriptPromises.shift();
           nextScriptPromise.done(scriptDetails => {
-            if (scriptDetails.contents) {
-              $.globalEval(scriptDetails.contents);
+            if (scriptDetails.url) {
+              const script = document.createElement('script');
+              script.src = scriptDetails.url;
+              script.onload = loadScriptSync;
+              script.onerror = loadScriptSync;
+              document.head.appendChild(script);
+            } else {
+              loadScriptSync();
             }
-            evalScriptSync();
           });
         } else {
-          // All evaluated
           promise.resolve($rawHtml.children());
         }
       };
 
-      evalScriptSync();
+      loadScriptSync();
       return promise;
     };
 
@@ -369,46 +369,63 @@ class OnePageViewModel {
         }
 
         $.ajax({
-          url:
-            baseURL +
-            (baseURL.indexOf('?') > -1 ? '&' : '?') +
-            'is_embeddable=true' +
-            self.extraEmbeddableURLParams(),
-          beforeSend: function (xhr) {
-            xhr.setRequestHeader('X-Requested-With', 'Hue');
-          },
+          url: `${baseURL}${baseURL.includes('?') ? '&' : '?'}is_embeddable=true${self.extraEmbeddableURLParams()}`,
+          beforeSend: xhr => xhr.setRequestHeader('X-Requested-With', 'Hue'),
           dataType: 'html',
-          success: function (response, status, xhr) {
+          success: (response, status, xhr) => {
             const type = xhr.getResponseHeader('Content-Type');
-            if (type.indexOf('text/') > -1) {
+            if (type.includes('text/')) {
               window.clearAppIntervals(app);
               huePubSub.clearAppSubscribers(app);
               self.extraEmbeddableURLParams('');
 
               self.processHeaders(response).done($rawHtml => {
-                if (window.SKIP_CACHE.indexOf(app) === -1) {
+                const scripts = $rawHtml.find('script');
+
+                // Append JSON scripts
+                scripts.each(function () {
+                  if (this.getAttribute('type') === 'application/json') {
+                    document.head.appendChild(this);
+                    // Disabling linting because initializeEditorComponent is defined in static folder
+                    // as this needs to be defined here
+                    // eslint-disable-next-line no-undef
+                    initializeEditorComponent();
+                  }
+                });
+
+                // Append other scripts
+                scripts.each(function () {
+                  if (!['application/json', 'text/html'].includes(this.getAttribute('type'))) {
+                    const script = document.createElement('script');
+                    script.type = 'text/javascript';
+                    if (this.src) {
+                      script.src = this.src;
+                      document.head.appendChild(script);
+                    }
+                    $(this).remove();
+                  }
+                });
+
+                if (!window.SKIP_CACHE.includes(app)) {
                   self.embeddable_cache[app] = $rawHtml;
                 }
-                $('#embeddable_' + app).html($rawHtml);
+                $(`#embeddable_${app}`).html($rawHtml);
                 huePubSub.publish('app.dom.loaded', app);
-                window.setTimeout(() => {
-                  self.isLoadingEmbeddable(false);
-                }, 0);
+                window.setTimeout(() => self.isLoadingEmbeddable(false), 0);
               });
-            } else {
-              if (type.indexOf('json') > -1) {
-                const presponse = JSON.parse(response);
-                if (presponse && presponse.url) {
-                  window.location.href = window.HUE_BASE_URL + presponse.url;
-                  return;
-                }
+            } else if (type.includes('json')) {
+              const presponse = JSON.parse(response);
+              if (presponse && presponse.url) {
+                window.location.href = `${window.HUE_BASE_URL}${presponse.url}`;
+                return;
               }
-              window.location.href = window.HUE_BASE_URL + baseURL;
+            } else {
+              window.location.href = `${window.HUE_BASE_URL}${baseURL}`;
             }
           },
-          error: function (xhr) {
+          error: xhr => {
             console.error('Route loading problem', xhr);
-            if ((xhr.status === 401 || xhr.status === 403) && app !== '403') {
+            if ([401, 403].includes(xhr.status) && app !== '403') {
               self.loadApp('403');
             } else if (app !== '500') {
               self.loadApp('500');

+ 7 - 0
desktop/core/src/desktop/lib/django_util.py

@@ -518,3 +518,10 @@ class JsonResponse(HttpResponse):
     kwargs.setdefault('content_type', 'application/json')
     data = json.dumps(data, cls=encoder, **json_dumps_params)
     super(JsonResponse, self).__init__(content=data, **kwargs)
+
+
+def nonce_attribute(request):
+  if hasattr(request, 'csp_nonce') and desktop.conf.CSP_NONCE.get():
+    csp_nonce = getattr(request, 'csp_nonce', None)
+    return f' nonce={csp_nonce}' if csp_nonce else ''
+  return ''

+ 108 - 0
desktop/core/src/desktop/middleware.py

@@ -17,6 +17,7 @@
 
 from __future__ import absolute_import
 
+import os
 import re
 import json
 import time
@@ -24,6 +25,7 @@ import socket
 import inspect
 import logging
 import os.path
+import secrets
 import tempfile
 import mimetypes
 import traceback
@@ -52,6 +54,7 @@ from desktop.auth.backend import ensure_has_a_group, find_or_create_user, is_adm
 from desktop.conf import (
   AUDIT_EVENT_LOG_DIR,
   AUTH,
+  CSP_NONCE,
   CUSTOM_CACHE_CONTROL,
   DJANGO_DEBUG_MODE,
   ENABLE_PROMETHEUS,
@@ -78,6 +81,69 @@ from hadoop import cluster
 from libsaml.conf import CDP_LOGOUT_URL
 from useradmin.models import User
 
+
+def nonce_exists(response):
+  """Check for preexisting nonce in style and script.
+
+    Args:
+      response (:obj:): Django response object
+
+    Returns:
+      nonce_found (dict): Dictionary of nonces found
+    """
+  try:
+    csp = response['Content-Security-Policy']
+  except KeyError:
+    csp = response.get('Content-Security-Policy-Report-Only', '')
+
+  nonce_found = {}
+
+  if csp:
+    csp_split = csp.split(';')
+    for directive in csp_split:
+      if 'nonce-' not in directive:
+        continue
+      if 'script-src' in directive:
+        nonce_found['script'] = directive
+      if 'style-src' in directive:
+        nonce_found['style'] = directive
+
+  return nonce_found
+
+
+def get_header(response):
+  """Get the CSP header type.
+
+  This is basically a check for:
+    Content-Security-Policy or Content-Security-Policy-Report-Only
+
+  Args:
+    response (:obj:): Django response object
+
+  Returns:
+    dict:
+      name: CPS header policy. i.e. Report-Only or not
+      csp: CSP directives associated with the header
+      bool: False if neither policy header is found
+  """
+  policies = [
+    "Content-Security-Policy",
+    "Content-Security-Policy-Report-Only"
+  ]
+
+  try:
+    name = policies[0]
+    csp = response[policies[0]]
+  except KeyError:
+    try:
+      name = policies[1]
+      csp = response[policies[1]]
+    except KeyError:
+      return False
+
+  return {'name': name, 'csp': csp}
+
+
 LOG = logging.getLogger()
 
 MIDDLEWARE_HEADER = "X-Hue-Middleware-Response"
@@ -805,10 +871,52 @@ class ContentSecurityPolicyMiddleware(MiddlewareMixin):
       LOG.info('Unloading ContentSecurityPolicyMiddleware')
       raise exceptions.MiddlewareNotUsed
 
+  def process_request(self, request):
+    nonce = secrets.token_urlsafe()
+    request.csp_nonce = nonce
+
   def process_response(self, request, response):
+    # If CSP_NONCE is not set, return the response without modification
+    if not CSP_NONCE.get():
+      return response
+
+    # Add the secure CSP if it doesn't exist, provided that we have a CSP to set
     if self.secure_content_security_policy and 'Content-Security-Policy' not in response:
       response["Content-Security-Policy"] = self.secure_content_security_policy
 
+    # If the CSP header is not set or the request does not have a nonce, return the response
+    header = get_header(response)
+    if not header or not hasattr(request, 'csp_nonce'):
+      return response
+
+    # If a nonce already exists in the CSP header, log an error and return the response
+    nonce_found = nonce_exists(response)
+    if nonce_found:
+      LOG.error("Nonce already exists: {}".format(nonce_found))
+      return response
+
+    # Retrieve the nonce from the request and prepare the new CSP directive
+    nonce = getattr(request, 'csp_nonce', None)
+    csp_split = header['csp'].split(';')
+    new_csp = []
+    nonce_directive = f"'nonce-{nonce}'"
+
+    for p in csp_split:
+      directive = p.lstrip().split(' ')[0]
+      if directive in ('script-src'):
+        # Remove 'unsafe-inline' if present
+        new_directive_parts = [
+            part for part in p.split(' ')
+            if part and part not in ("'unsafe-inline'")
+        ]
+        new_directive_parts.append(nonce_directive)
+        new_csp.append(' '.join(new_directive_parts))
+      else:
+        new_csp.append(p)
+
+    # Update the Content-Security-Policy header with the new CSP string
+    response[header['name']] = "; ".join(new_csp).strip() + ';'
+
     return response
 
 

+ 233 - 0
desktop/core/src/desktop/static/desktop/js/editor-component.js

@@ -0,0 +1,233 @@
+function initializeEditorComponent() {
+    // Fetches data as text content from a document inserted by onePageViewModel.
+    // This approach supports 'unsafe-inline' by embedding the content in the <head>,
+    // as specified in editor_component.mako.
+    const editorOptionsElement = document.getElementById('editorOptionsJson');
+    let options;
+    try {
+        const optionsJson = editorOptionsElement.textContent;
+        options = JSON.parse(optionsJson);
+    } catch (error) {
+        console.error('Failed to parse editor options JSON:', error);
+        return;
+    }
+    const user = {
+        username: window.LOGGED_USERNAME,
+        id: window.LOGGED_USER_ID
+    };
+    
+    var ENABLE_QUERY_SCHEDULING = window.ENABLE_QUERY_SCHEDULING || false;
+    var OPTIMIZER = {
+        AUTO_UPLOAD_QUERIES: window.AUTO_UPLOAD_SQL_ANALYZER_STATS || false,
+        AUTO_UPLOAD_DDL: window.AUTO_UPLOAD_SQL_ANALYZER_STATS || false,
+        QUERY_HISTORY_UPLOAD_LIMIT: window.QUERY_HISTORY_UPLOAD_LIMIT
+    };
+
+    window.EDITOR_BINDABLE_ELEMENT = '#editorComponents';
+
+    window.EDITOR_SUFFIX = 'editor';
+
+    var HUE_PUB_SUB_EDITOR_ID = (window.location.pathname.indexOf('notebook') > -1) ? 'notebook' : 'editor';
+
+    window.EDITOR_VIEW_MODEL_OPTIONS = $.extend(options, {
+        huePubSubId: HUE_PUB_SUB_EDITOR_ID,
+        user: user.username,
+        userId: user.id,
+        suffix: window.EDITOR_SUFFIX,
+        assistAvailable: true,
+        snippetViewSettings: {
+            default: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/sql',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            code: {
+                placeHolder: I18n("Example: 1 + 1, or press CTRL + space"),
+                snippetIcon: 'fa-code'
+            },
+            hive: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/hive',
+                snippetImage: '${ static("beeswax/art/icon_beeswax_48.png") }',
+                sqlDialect: true
+            },
+            hplsql: {
+                placeHolder: I18n("Example: CREATE PROCEDURE name AS SELECT * FROM tablename limit 10 GO"),
+                aceMode: 'ace/mode/hplsql',
+                snippetImage: '${ static("beeswax/art/icon_beeswax_48.png") }',
+                sqlDialect: true
+            },
+            impala: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/impala',
+                snippetImage: '${ static("impala/art/icon_impala_48.png") }',
+                sqlDialect: true
+            },
+            presto: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/presto',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            dasksql: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/dasksql',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            elasticsearch: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/elasticsearch',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            druid: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/druid',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            bigquery: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/bigquery',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            phoenix: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/phoenix',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            ksql: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/ksql',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            flink: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/flink',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            jar: {
+                snippetIcon: 'fa-file-archive-o '
+            },
+            mysql: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/mysql',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            mysqljdbc: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/mysql',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            oracle: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/oracle',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            pig: {
+                placeHolder: I18n("Example: 1 + 1, or press CTRL + space"),
+                aceMode: 'ace/mode/pig',
+                snippetImage: '${ static("pig/art/icon_pig_48.png") }'
+            },
+            postgresql: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/pgsql',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            solr: {
+                placeHolder: I18n("Example: SELECT fieldA, FieldB FROM collectionname, or press CTRL + space"),
+                aceMode: 'ace/mode/mysql',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            kafkasql: {
+                placeHolder: I18n("Example: SELECT fieldA, FieldB FROM collectionname, or press CTRL + space"),
+                aceMode: 'ace/mode/mysql',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            java: {
+                snippetIcon: 'fa-file-code-o'
+            },
+            py: {
+                snippetIcon: 'fa-file-code-o'
+            },
+            pyspark: {
+                placeHolder: I18n("Example: 1 + 1, or press CTRL + space"),
+                aceMode: 'ace/mode/python',
+                snippetImage: '${ static("spark/art/icon_spark_48.png") }'
+            },
+            r: {
+                placeHolder: I18n("Example: 1 + 1, or press CTRL + space"),
+                aceMode: 'ace/mode/r',
+                snippetImage: '${ static("spark/art/icon_spark_48.png") }'
+            },
+            scala: {
+                placeHolder: I18n("Example: 1 + 1, or press CTRL + space"),
+                aceMode: 'ace/mode/scala',
+                snippetImage: '${ static("spark/art/icon_spark_48.png") }'
+            },
+            spark: {
+                placeHolder: I18n("Example: 1 + 1, or press CTRL + space"),
+                aceMode: 'ace/mode/scala',
+                snippetImage: '${ static("spark/art/icon_spark_48.png") }'
+            },
+            spark2: {
+                snippetImage: '${ static("spark/art/icon_spark_48.png") }'
+            },
+            sparksql: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/sparksql',
+                snippetImage: '${ static("spark/art/icon_spark_48.png") }',
+                sqlDialect: true
+            },
+            mapreduce: {
+                snippetIcon: 'fa-file-archive-o'
+            },
+            shell: {
+                snippetIcon: 'fa-terminal'
+            },
+            sqoop1: {
+                placeHolder: I18n("Example: import  --connect jdbc:hsqldb:file:db.hsqldb --table TT --target-dir hdfs://localhost:8020/user/foo -m 1"),
+                snippetImage: '${ static("sqoop/art/icon_sqoop_48.png") }'
+            },
+            distcp: {
+                snippetIcon: 'fa-files-o'
+            },
+            sqlite: {
+                placeHolder: I18n("Example: SELECT * FROM tablename, or press CTRL + space"),
+                aceMode: 'ace/mode/sql',
+                snippetIcon: 'fa-database',
+                sqlDialect: true
+            },
+            text: {
+                placeHolder: I18n('Type your text here'),
+                aceMode: 'ace/mode/text',
+                snippetIcon: 'fa-header'
+            },
+            markdown: {
+                placeHolder: I18n('Type your markdown here'),
+                aceMode: 'ace/mode/markdown',
+                snippetIcon: 'fa-header'
+            }
+        }
+    });
+
+    window.EDITOR_ENABLE_QUERY_SCHEDULING = ENABLE_QUERY_SCHEDULING;
+
+    window.SQL_ANALYZER_AUTO_UPLOAD_QUERIES = OPTIMIZER.AUTO_UPLOAD_QUERIES;
+
+    window.SQL_ANALYZER_AUTO_UPLOAD_DDL = OPTIMIZER.AUTO_UPLOAD_DDL;
+
+    window.SQL_ANALYZER_QUERY_HISTORY_UPLOAD_LIMIT = OPTIMIZER.QUERY_HISTORY_UPLOAD_LIMIT;
+}

+ 2 - 1
desktop/core/src/desktop/templates/common_footer.mako

@@ -19,6 +19,7 @@ from django.http import HttpRequest
 
 from desktop.lib.i18n import smart_str
 from desktop.views import login_modal
+from desktop.lib.django_util import nonce_attribute
 
 if sys.version_info[0] > 2:
   from django.utils.translation import gettext as _
@@ -34,7 +35,7 @@ ${ smart_str(login_modal(request).content) | n,unicode }
 
 <iframe id="zoomDetectFrame" style="width: 250px; display: none" ></iframe>
 
-${ commonHeaderFooterComponents.footer(messages) }
+${ commonHeaderFooterComponents.footer(messages, nonce_attribute(request) ) }
 
   </body>
 </html>

+ 4 - 3
desktop/core/src/desktop/templates/common_header_footer_components.mako

@@ -24,6 +24,7 @@ from desktop.views import _ko
 from beeswax.conf import LIST_PARTITIONS_LIMIT
 from indexer.conf import ENABLE_NEW_INDEXER
 from metadata.conf import has_optimizer, OPTIMIZER
+from desktop.lib.django_util import nonce_attribute
 
 if sys.version_info[0] > 2:
   from django.utils.translation import gettext as _
@@ -32,7 +33,7 @@ else:
 %>
 
 <%def name="header_pollers(user, is_s3_enabled, apps)">
-  <script type="text/javascript">
+  <script type="text/javascript" ${nonce_attribute(request)}>
     Dropzone.autoDiscover = false;
     moment.locale(window.navigator.userLanguage || window.navigator.language);
     localeFormat = function (time) {
@@ -229,7 +230,7 @@ else:
 
 </%def>
 
-<%def name="footer(messages)">
+<%def name="footer(messages, nonce)">
 
 <div id="progressStatus" class="uploadstatus well hide">
   <h4>${ _('Upload progress') }</h4>
@@ -266,7 +267,7 @@ else:
 
 <div class="clipboard-content"></div>
 
-<script type="text/javascript">
+<script type="text/javascript" ${nonce_attribute(request)}>
 
   $(document).ready(function () {
 

+ 4 - 3
desktop/core/src/desktop/templates/common_notebook_ko_components.mako

@@ -28,6 +28,7 @@ if sys.version_info[0] > 2:
   from django.utils.translation import gettext as _
 else:
   from django.utils.translation import ugettext as _
+from desktop.lib.django_util import nonce_attribute
 %>
 
 
@@ -73,7 +74,7 @@ else:
     </div>
   </script>
 
-  <script type="text/javascript">
+  <script type="text/javascript"  ${nonce_attribute(request)}>
     (function () {
       var WHEEL_RADIUS = 75;
       var PLUS_ICON_RADIUS = 27.859; // FA-5X
@@ -352,7 +353,7 @@ else:
     </div>
   </script>
 
-  <script type="text/javascript">
+  <script type="text/javascript" ${nonce_attribute(request)} >
     (function () {
 
       function DownloadResultsViewModel (params, element) {
@@ -777,7 +778,7 @@ else:
     </div>
   </script>
 
-  <script type="text/javascript">
+  <script type="text/javascript"  ${nonce_attribute(request)} >
     (function () {
 
       function AceKeyboardShortcutsViewModel () {

+ 10 - 9
desktop/core/src/desktop/templates/config_ko_components.mako

@@ -23,6 +23,7 @@ if sys.version_info[0] > 2:
   from django.utils.translation import gettext as _
 else:
   from django.utils.translation import ugettext as _
+from desktop.lib.django_util import nonce_attribute
 %>
 
 <%def name="config()">
@@ -123,7 +124,7 @@ else:
     </div>
   </script>
 
-  <script type="text/javascript">
+  <script ${nonce_attribute(request)} type="text/javascript">
     (function () {
       function MultiGroupAlternative(alt, params, initiallyChecked) {
         var self = this;
@@ -214,7 +215,7 @@ else:
     })();
   </script>
 
-  <script type="text/javascript">
+  <script  ${nonce_attribute(request)} type="text/javascript">
     (function () {
 
       function PropertySelectorViewModel(params) {
@@ -296,7 +297,7 @@ else:
     })();
   </script>
 
-  <script type="text/html" id="property">
+  <script  ${nonce_attribute(request)} type="text/html" id="property">
     <div class="config-property" data-bind="visibleOnHover: { selector: '.hover-actions' }">
       <label class="config-label" data-bind="click: function(data, event){ $(event.target).siblings('.config-controls').find('.config-property-add-value a').click(); }">
         <!-- ko text: label --><!-- /ko --><!-- ko if: typeof helpText !== 'undefined' --><div class="property-help" data-bind="tooltip: { title: helpText(), placement: 'bottom' }"><i class="fa fa-question-circle-o"></i></div><!-- /ko -->
@@ -364,7 +365,7 @@ else:
     <input type="text" class="input-small" data-bind="numericTextInput: { value: value, precision: 0, allowEmpty: true }" /> <select class="input-mini" data-bind="options: units, value: selectedUnit"></select>
   </script>
 
-  <script type="text/javascript">
+  <script  ${nonce_attribute(request)}  type="text/javascript">
     (function () {
       var JVM_MEM_PATTERN = /([0-9]+)([MG])$/;
       var UNITS = {'MB': 'M', 'GB': 'G'};
@@ -426,7 +427,7 @@ else:
     <div class="clearfix"></div>
   </script>
 
-  <script type="text/javascript">
+  <script  ${nonce_attribute(request)} type="text/javascript">
     (function () {
 
       function KeyValueListInputViewModel(params) {
@@ -529,7 +530,7 @@ else:
     </div>
   </script>
 
-  <script type="text/javascript">
+  <script  ${nonce_attribute(request)} type="text/javascript">
     (function () {
 
       function NameValueListInputViewModel(params) {
@@ -594,7 +595,7 @@ else:
     </div>
   </script>
 
-  <script type="text/javascript">
+  <script  ${nonce_attribute(request)} type="text/javascript">
     (function () {
 
       function FunctionListInputViewModel(params) {
@@ -648,7 +649,7 @@ else:
     </div>
   </script>
 
-  <script type="text/javascript">
+  <script  ${nonce_attribute(request)}  type="text/javascript">
     (function () {
 
       var identifyType = function (path) {
@@ -728,7 +729,7 @@ else:
     </div>
   </script>
 
-  <script type="text/javascript">
+  <script  ${nonce_attribute(request)} type="text/javascript">
     (function () {
       function CsvListInputViewModel(params) {
         this.valueObservable = params.value;

+ 6 - 5
desktop/core/src/desktop/templates/hue.mako

@@ -24,6 +24,7 @@
   from desktop.models import hue_version
   from desktop.views import _ko, commonshare, login_modal
   from desktop.webpack_utils import get_hue_bundles
+  from desktop.lib.django_util import nonce_attribute
 
   from webpack_loader.templatetags.webpack_loader import render_bundle
 
@@ -44,7 +45,7 @@
   % if conf.COLLECT_USAGE.get():
     <!-- Google tag (gtag.js) -->
     <script async src="https://www.googletagmanager.com/gtag/js?id=${conf.GTAG_ID.get()}"></script>
-    <script>
+    <script ${nonce_attribute(request)}>
       window.dataLayer = window.dataLayer || [];
       function gtag(){dataLayer.push(arguments);}
       gtag('js', new Date());
@@ -110,8 +111,8 @@
   </ul>
 
   <!-- UserVoice JavaScript SDK -->
-  <script>(function(){var uv=document.createElement('script');uv.type='text/javascript';uv.async=true;uv.src='//widget.uservoice.com/8YpsDfIl1Y2sNdONoLXhrg.js';var s=document.getElementsByTagName('script')[0];s.parentNode.insertBefore(uv,s)})()</script>
-  <script>
+  <script ${nonce_attribute(request)}>(function(){var uv=document.createElement('script');uv.type='text/javascript';uv.async=true;uv.src='//widget.uservoice.com/8YpsDfIl1Y2sNdONoLXhrg.js';var s=document.getElementsByTagName('script')[0];s.parentNode.insertBefore(uv,s)})()</script>
+  <script ${nonce_attribute(request)}>
   UserVoice = window.UserVoice || [];
   function showClassicWidget() {
     UserVoice.push(['showLightbox', 'classic_widget', {
@@ -312,7 +313,7 @@ ${ commonshare() | n,unicode }
 
 <script src="${ static('desktop/js/share2.vm.js') }"></script>
 
-<script>
+<script ${nonce_attribute(request)}  >
   var shareViewModel = initSharing("#documentShareModal");
 </script>
 
@@ -334,7 +335,7 @@ ${ smart_str(login_modal(request).content) | n,unicode }
 
 <iframe id="zoomDetectFrame" style="width: 250px; display: none" ></iframe>
 
-${ commonHeaderFooterComponents.footer(messages) }
+${ commonHeaderFooterComponents.footer(messages, nonce) }
 
 ## This includes common knockout templates that are shared with the Job Browser page and the mini job browser panel
 ## available in the upper right corner throughout Hue

+ 3 - 2
desktop/core/src/desktop/templates/hue_ace_autocompleter.mako

@@ -23,6 +23,7 @@ if sys.version_info[0] > 2:
   from django.utils.translation import gettext as _
 else:
   from django.utils.translation import ugettext as _
+from desktop.lib.django_util import nonce_attribute
 %>
 
 <%def name="hueAceAutocompleter()">
@@ -356,7 +357,7 @@ else:
   </script>
 
 
-  <script type="text/javascript">
+  <script type="text/javascript" ${nonce_attribute(request)}>
     (function () {
 
       var aceUtil = ace.require('ace/autocomplete/util');
@@ -722,7 +723,7 @@ else:
     <!-- /ko -->
   </script>
 
-  <script type="text/javascript">
+  <script type="text/javascript" ${nonce_attribute(request)}>
     (function () {
 
       var COMMENT_LOAD_DELAY = 1500;

+ 3 - 3
desktop/core/src/desktop/templates/login.mako

@@ -21,11 +21,11 @@
 
   from desktop.conf import CUSTOM, ENABLE_ORGANIZATIONS
   from desktop.views import commonheader, commonfooter
-
   if sys.version_info[0] > 2:
     from django.utils.translation import gettext as _
   else:
     from django.utils.translation import ugettext as _
+  from desktop.lib.django_util import nonce_attribute
 %>
 
 <%namespace name="hueIcons" file="/hue_icons.mako" />
@@ -161,7 +161,7 @@ ${ commonheader(_("Welcome to Hue"), "login", user, request, "50px", True, True)
    % endif
 </div>
 
-<script>
+<script ${nonce_attribute(request)}>
   $(document).ready(function () {
     $("form").on("submit", function () {
       window.setTimeout(function () {
@@ -195,4 +195,4 @@ ${ commonheader(_("Welcome to Hue"), "login", user, request, "50px", True, True)
   });
 </script>
 
-${ commonfooter(None, messages) | n,unicode }
+${ commonfooter(request, messages, nonce ) | n,unicode }

+ 2 - 1
desktop/core/src/desktop/templates/login_modal.mako

@@ -24,6 +24,7 @@
     from django.utils.translation import ugettext as _
 
   from useradmin.hue_password_policy import is_password_policy_enabled, get_password_hint
+  from desktop.lib.django_util import nonce_attribute
 %>
 
 
@@ -49,7 +50,7 @@
   </div>
 </div>
 
-<script>
+<script ${nonce_attribute(request)}>
   $(document).ready(function () {
     $('.reload').on('click', function () {
       location.reload();

+ 8 - 211
desktop/libs/notebook/src/notebook/templates/editor_components.mako

@@ -27,7 +27,7 @@ from desktop.webpack_utils import get_hue_bundles
 from metadata.conf import has_optimizer, OPTIMIZER
 
 from notebook.conf import ENABLE_QUERY_BUILDER, ENABLE_QUERY_SCHEDULING, ENABLE_BATCH_EXECUTE, ENABLE_EXTERNAL_STATEMENT, ENABLE_PRESENTATION
-
+from desktop.lib.django_util import nonce_attribute
 if sys.version_info[0] > 2:
   from django.utils.translation import gettext as _
 else:
@@ -1899,6 +1899,7 @@ else:
   </div>
 </script>
 
+
 <div class="ace-filechooser" style="display:none;">
   <div class="ace-filechooser-close">
     <a class="pointer" data-bind="click: function(){ $('.ace-filechooser').hide(); }"><i class="fa fa-times"></i></a>
@@ -2052,215 +2053,11 @@ else:
 
 
 <%def name="commonJS(is_embeddable=False, bindableElement='editorComponents', suffix='')">
+  <script type="application/json" id="editorOptionsJson">
+    ${ options_json | n,unicode,antixss }
+  </script>
+  <script ${nonce_attribute(request)} src="${ static('desktop/js/editor-component.js') }"></script>
+</%def>
 
-<script type="text/javascript">
-  window.EDITOR_BINDABLE_ELEMENT = '#${ bindableElement }';
-
-  window.EDITOR_SUFFIX = '${ suffix }';
-
-  var HUE_PUB_SUB_EDITOR_ID = (window.location.pathname.indexOf('notebook') > -1) ? 'notebook' : 'editor';
-
-  window.EDITOR_VIEW_MODEL_OPTIONS = $.extend(${ options_json | n,unicode,antixss }, {
-    huePubSubId: HUE_PUB_SUB_EDITOR_ID,
-    user: '${ user.username }',
-    userId: ${ user.id },
-    suffix: '${ suffix }',
-    assistAvailable: true,
-    snippetViewSettings: {
-      default: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/sql',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      code: {
-        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-        snippetIcon: 'fa-code'
-      },
-      hive: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/hive',
-        snippetImage: '${ static("beeswax/art/icon_beeswax_48.png") }',
-        sqlDialect: true
-      },
-      hplsql: {
-        placeHolder: '${ _("Example: CREATE PROCEDURE name AS SELECT * FROM tablename limit 10 GO") }',
-        aceMode: 'ace/mode/hplsql',
-        snippetImage: '${ static("beeswax/art/icon_beeswax_48.png") }',
-        sqlDialect: true
-      },
-      impala: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/impala',
-        snippetImage: '${ static("impala/art/icon_impala_48.png") }',
-        sqlDialect: true
-      },
-      presto: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/presto',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      dasksql: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/dasksql',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      elasticsearch: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/elasticsearch',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      druid: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/druid',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      bigquery: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/bigquery',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      phoenix: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/phoenix',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      ksql: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/ksql',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      flink: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/flink',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      jar : {
-        snippetIcon: 'fa-file-archive-o '
-      },
-      mysql: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/mysql',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      mysqljdbc: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/mysql',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      oracle: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/oracle',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      pig: {
-        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-        aceMode: 'ace/mode/pig',
-        snippetImage: '${ static("pig/art/icon_pig_48.png") }'
-      },
-      postgresql: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/pgsql',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      solr: {
-        placeHolder: '${ _("Example: SELECT fieldA, FieldB FROM collectionname, or press CTRL + space") }',
-        aceMode: 'ace/mode/mysql',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      kafkasql: {
-        placeHolder: '${ _("Example: SELECT fieldA, FieldB FROM collectionname, or press CTRL + space") }',
-        aceMode: 'ace/mode/mysql',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      java : {
-        snippetIcon: 'fa-file-code-o'
-      },
-      py : {
-        snippetIcon: 'fa-file-code-o'
-      },
-      pyspark: {
-        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-        aceMode: 'ace/mode/python',
-        snippetImage: '${ static("spark/art/icon_spark_48.png") }'
-      },
-      r: {
-        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-        aceMode: 'ace/mode/r',
-        snippetImage: '${ static("spark/art/icon_spark_48.png") }'
-      },
-      scala: {
-        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-        aceMode: 'ace/mode/scala',
-        snippetImage: '${ static("spark/art/icon_spark_48.png") }'
-      },
-      spark: {
-        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-        aceMode: 'ace/mode/scala',
-        snippetImage: '${ static("spark/art/icon_spark_48.png") }'
-      },
-      spark2: {
-        snippetImage: '${ static("spark/art/icon_spark_48.png") }'
-      },
-      sparksql: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/sparksql',
-        snippetImage: '${ static("spark/art/icon_spark_48.png") }',
-        sqlDialect: true
-      },
-      mapreduce: {
-        snippetIcon: 'fa-file-archive-o'
-      },
-      shell: {
-        snippetIcon: 'fa-terminal'
-      },
-      sqoop1: {
-        placeHolder: '${ _("Example: import  --connect jdbc:hsqldb:file:db.hsqldb --table TT --target-dir hdfs://localhost:8020/user/foo -m 1") }',
-        snippetImage: '${ static("sqoop/art/icon_sqoop_48.png") }'
-      },
-      distcp: {
-        snippetIcon: 'fa-files-o'
-      },
-      sqlite: {
-        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-        aceMode: 'ace/mode/sql',
-        snippetIcon: 'fa-database',
-        sqlDialect: true
-      },
-      text: {
-        placeHolder: '${ _('Type your text here') }',
-        aceMode: 'ace/mode/text',
-        snippetIcon: 'fa-header'
-      },
-      markdown: {
-        placeHolder: '${ _('Type your markdown here') }',
-        aceMode: 'ace/mode/markdown',
-        snippetIcon: 'fa-header'
-      }
-    }
-  });
-
-  window.EDITOR_ENABLE_QUERY_SCHEDULING = '${ ENABLE_QUERY_SCHEDULING.get() }' === 'True';
-
-  window.SQL_ANALYZER_AUTO_UPLOAD_QUERIES = '${ OPTIMIZER.AUTO_UPLOAD_QUERIES.get() }' === 'True';
-
-  window.SQL_ANALYZER_AUTO_UPLOAD_DDL = '${ OPTIMIZER.AUTO_UPLOAD_DDL.get() }' === 'True';
-
-  window.SQL_ANALYZER_QUERY_HISTORY_UPLOAD_LIMIT = ${ OPTIMIZER.QUERY_HISTORY_UPLOAD_LIMIT.get() };
-</script>
 
-</%def>
+