Skip to content

Commit

Permalink
csp: nonce and unsafe-eval for scripts
Browse files Browse the repository at this point in the history
To kick things off, a rudimentary CSP implementation only allows
dynamically loading new JavaScript if it includes an associated nonce
that is generated on every load of the app.

A more sophisticated content security policy is necessary, particularly
one that bans eval for scripts, but one step at a time.
  • Loading branch information
epixa committed Jan 31, 2019
1 parent c443fee commit 9f40321
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 13 deletions.
5 changes: 2 additions & 3 deletions packages/kbn-interpreter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"@kbn/i18n": "1.0.0",
"lodash": "npm:@elastic/lodash@3.10.1-kibana1",
"lodash.clone": "^4.5.0",
"scriptjs": "^2.5.8",
"socket.io-client": "^2.1.1",
"uuid": "3.0.1"
},
Expand All @@ -24,8 +23,8 @@
"babel-loader": "7.1.5",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "6.20.0",
"css-loader": "1.0.0",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "1.0.0",
"del": "^3.0.0",
"getopts": "^2.2.3",
"pegjs": "0.9.0",
Expand All @@ -36,4 +35,4 @@
"webpack": "4.23.1",
"webpack-cli": "^3.1.2"
}
}
}
17 changes: 15 additions & 2 deletions packages/kbn-interpreter/src/public/browser_registries.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,20 @@
*/

import { i18n } from '@kbn/i18n';
import $script from 'scriptjs';

function loadPath(path, callback) {
const script = document.createElement('script');

script.setAttribute('async', '');
script.setAttribute('nonce', window.__webpack_nonce__);
script.addEventListener('error', () => {
console.error('Failed to load plugin bundle', path);
});
script.setAttribute('src', path);
script.addEventListener('load', callback);

document.head.appendChild(script);
}

export const loadBrowserRegistries = (registries, basePath) => {
const remainingTypes = Object.keys(registries);
Expand All @@ -38,7 +51,7 @@ export const loadBrowserRegistries = (registries, basePath) => {
// Load plugins one at a time because each needs a different loader function
// $script will only load each of these once, we so can call this as many times as we need?
const pluginPath = `${basePath}/api/canvas/plugins?type=${type}`;
$script(pluginPath, () => {
loadPath(pluginPath, () => {
populatedTypes[type] = registries[type];
loadType();
});
Expand Down
1 change: 1 addition & 0 deletions src/ui/ui_render/bootstrap/template.js.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ window.onload = function () {
var dom = document.createElement('script');

dom.setAttribute('async', '');
dom.setAttribute('nonce', window.__webpack_nonce__);
dom.addEventListener('error', failure);
dom.setAttribute('src', file);
dom.addEventListener('load', next);
Expand Down
21 changes: 19 additions & 2 deletions src/ui/ui_render/ui_render_mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
* under the License.
*/

import { createHash } from 'crypto';
import { createHash, randomBytes } from 'crypto';
import { promisify } from 'util';
import { props, reduce as reduceAsync } from 'bluebird';
import Boom from 'boom';
import { resolve } from 'path';
Expand All @@ -27,6 +28,8 @@ import { AppBootstrap } from './bootstrap';
import { mergeVariables } from './lib';
import { fromRoot } from '../../utils';

const randomBytesAsync = promisify(randomBytes);

export function uiRenderMixin(kbnServer, server, config) {
function replaceInjectedVars(request, injectedVars) {
const { injectedVarsReplacers = [] } = kbnServer.uiExports;
Expand Down Expand Up @@ -212,7 +215,10 @@ export function uiRenderMixin(kbnServer, server, config) {
injectedVarsOverrides
});

return h.view('ui_app', {
const nonce = (await randomBytesAsync(12)).toString('base64');

const response = h.view('ui_app', {
nonce,
uiPublicUrl: `${basePath}/ui`,
bootstrapScriptUrl: `${basePath}/bundles/app/${app.getId()}/bootstrap.js`,
i18n: (id, options) => i18n.translate(id, options),
Expand All @@ -238,6 +244,17 @@ export function uiRenderMixin(kbnServer, server, config) {
legacyMetadata,
},
});

const csp = [
`script-src 'unsafe-eval' 'nonce-${nonce}'`,
'worker-src blob:',
'child-src blob:',
'img-src * data: blob:',
];

response.header('content-security-policy', csp.join(';'));

return response;
}

server.decorate('toolkit', 'renderApp', function (app, injectedVarsOverrides) {
Expand Down
4 changes: 3 additions & 1 deletion src/ui/ui_render/views/ui_app.pug
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,6 @@ block content
.kibanaWelcomeText(data-error-message=i18n('common.ui.welcomeErrorMessage', { defaultMessage: 'Kibana did not load properly. Check the server output for more information.' }))
| #{i18n('common.ui.welcomeMessage', { defaultMessage: 'Loading Kibana' })}

script(src=bootstrapScriptUrl)
script(nonce=nonce).
window.__webpack_nonce__ = '!{nonce}';
script(src=bootstrapScriptUrl, nonce=nonce)
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -19135,11 +19135,6 @@ script-loader@0.7.2:
dependencies:
raw-loader "~0.5.1"

scriptjs@^2.5.8:
version "2.5.8"
resolved "https://registry.yarnpkg.com/scriptjs/-/scriptjs-2.5.8.tgz#d0c43955c2e6bad33b6e4edf7b53b8965aa7ca5f"
integrity sha1-0MQ5VcLmutM7bk7fe1O4llqnyl8=

scroll-into-view@^1.3.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/scroll-into-view/-/scroll-into-view-1.9.1.tgz#90c3b338422f9fddaebad90e6954790940dc9c1e"
Expand Down

0 comments on commit 9f40321

Please sign in to comment.