Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[security] Unsafe Inline Script #3876

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions desktop/conf.dist/hue.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: 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
Expand Down
4 changes: 4 additions & 0 deletions desktop/conf/pseudo-distributed.ini.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: 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
Expand Down
7 changes: 7 additions & 0 deletions desktop/core/src/desktop/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,13 @@ def has_ssl_no_renegotiation():
"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.'),
Expand Down
105 changes: 61 additions & 44 deletions desktop/core/src/desktop/js/onePageViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,12 @@ class OnePageViewModel {
$.ajax({
url: scriptUrl,
converters: {
'text script': function (text) {
return text;
}
'text script': text => text
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, was this change needed or just an improvement?

}
})
.done(contents => {
loadedJs.push(scriptUrl);
deferred.resolve({ url: scriptUrl, contents: contents });
deferred.resolve({ url: scriptUrl, contents });
})
.fail(() => {
deferred.resolve('');
Expand All @@ -201,29 +199,28 @@ 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 });
}
$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);
Expand All @@ -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;
};

Expand Down Expand Up @@ -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');
Expand Down
9 changes: 9 additions & 0 deletions desktop/core/src/desktop/lib/django_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,3 +518,12 @@ def __init__(self, data, encoder=DjangoJSONEncoder, safe=True,
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):
# Assuming 'csp_nonce' is set in the request and CSP_NONCE controls its usage
if desktop.conf.CSP_NONCE.get():
Comment on lines +524 to +525
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of assuming, why not check for it by using hasattr()?

csp_nonce = request.csp_nonce
if csp_nonce:
return f' nonce={csp_nonce}'
return ''
Comment on lines +527 to +529
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Try something like this once?

return f' nonce={csp_nonce}' if csp_nonce else ''

Loading