From e5fe84a1c8d6b5417cf83f632f5dcc6ea2760578 Mon Sep 17 00:00:00 2001 From: Jeldrik Hanschke Date: Mon, 16 Sep 2019 21:45:53 +0200 Subject: [PATCH] set CSP header in FastBoot --- .../content-security-policy.js | 34 ++ .../content-security-policy.js | 1 + index.js | 83 +++-- node-tests/e2e/fastboot-support-test.js | 122 ++++++ package.json | 5 +- yarn.lock | 352 +++++++++++++++++- 6 files changed, 555 insertions(+), 42 deletions(-) create mode 100644 addon/instance-initializers/content-security-policy.js create mode 100644 fastboot/instance-initializers/content-security-policy.js create mode 100644 node-tests/e2e/fastboot-support-test.js diff --git a/addon/instance-initializers/content-security-policy.js b/addon/instance-initializers/content-security-policy.js new file mode 100644 index 0000000..b4ee750 --- /dev/null +++ b/addon/instance-initializers/content-security-policy.js @@ -0,0 +1,34 @@ +import { assert } from '@ember/debug'; + +// reads addon config stored in meta element +function readAddonConfig(appInstance) { + let config = appInstance.resolveRegistration('config:environment'); + let addonConfig = config['ember-cli-content-security-policy']; + + // TODO: do not require policy to be stored in config object + // if already available through CSP meta element + assert( + 'Required configuration is available at run-time', + addonConfig.hasOwnProperty('reportOnly') && addonConfig.hasOwnProperty('policy') + ); + + return config['ember-cli-content-security-policy']; +} + +export function initialize(appInstance) { + let fastboot = appInstance.lookup('service:fastboot'); + + if (!fastboot || !fastboot.get('isFastBoot')) { + // nothing to do if application does not run in FastBoot or + // does not even have a FastBoot service + return; + } + + let { policy, reportOnly } = readAddonConfig(appInstance); + let header = reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'; + fastboot.get('response.headers').set(header, policy); +} + +export default { + initialize +}; diff --git a/fastboot/instance-initializers/content-security-policy.js b/fastboot/instance-initializers/content-security-policy.js new file mode 100644 index 0000000..5f59902 --- /dev/null +++ b/fastboot/instance-initializers/content-security-policy.js @@ -0,0 +1 @@ +export { default, initialize } from 'ember-cli-content-security-policy/instance-initializers/content-security-policy'; diff --git a/index.js b/index.js index 299cff1..4b0baea 100644 --- a/index.js +++ b/index.js @@ -49,6 +49,55 @@ let allowLiveReload = function(policyObject, liveReloadConfig) { module.exports = { name: require('./package').name, + // Configuration is only available by public API in `app` passed to some hook. + // We calculate configuration in `config` hook and use it in `serverMiddleware` + // and `contentFor` hooks, which are executed later. This prevents us from needing to + // calculate the config more than once. We can't do this in `contentFor` hook cause + // that one is executed after `serverMiddleware` and can't do it in `serverMiddleware` + // hook cause that one is only executed on `ember serve` but not on `ember build` or + // `ember test`. We can't do it in `init` hook cause app is not available by then. + // + // The same applies to policy string generation. It's also calculated in `config` + // hook and reused in both others. But this one might be overriden in `serverMiddleware` + // hook to support live reload. This is safe because `serverMiddleware` hook is executed + // before `contentFor` hook. + // + // Only a small subset of the configuration is required at run time in order to support + // FastBoot. This one is returned here as default configuration in order to make it + // available at run time. + config: function(environment, runConfig) { + // calculate configuration and policy string + // hook may be called more than once, but we only need to calculate once + if (!this._config) { + let { app, project } = this; + let ownConfig = readConfig(project, environment); + let ui = project.ui; + let config = calculateConfig(environment, ownConfig, runConfig, ui); + + // add static test nonce if build includes tests + // Note: app is not defined for CLI commands + if (app && app.tests) { + appendSourceList(config.policy, 'script-src', `'nonce-${STATIC_TEST_NONCE}'`); + } + + this._config = config; + this._policyString = buildPolicyString(config.policy); + } + + // provide configuration needed at run-time for FastBoot support (if needed) + // TODO: only inject if application uses FastBoot + if (!this._config.enabled || !this._config.delivery.includes('header')) { + return {}; + } + + return { + 'ember-cli-content-security-policy': { + policy: this._policyString, + reportOnly: this._config.reportOnly, + }, + }; + }, + serverMiddleware: function({ app, options }) { // Configuration is not changeable at run-time. Therefore it's safe to not // register the express middleware at all if addon is disabled and @@ -115,6 +164,7 @@ module.exports = { return; } + // inject CSP meta tag if (type === 'head' && this._config.delivery.indexOf(DELIVERY_META) !== -1) { this.ui.writeWarnLine( 'Content Security Policy does not support report only mode if delivered via meta element. ' + @@ -132,6 +182,7 @@ module.exports = { return ``; } + // inject event listener needed for test support if (type === 'test-body' && this._config.failTests) { let qunitDependency = (new VersionChecker(this)).for('qunit'); if (qunitDependency.exists() && qunitDependency.lt('2.9.2')) { @@ -157,8 +208,8 @@ module.exports = { `; } + // Add nonce to