ember-cli-content-security-policy
Version:
This addon adds the Content-Security-Policy header to response sent from the Ember CLI Express server.
186 lines (162 loc) • 5.87 kB
JavaScript
/* eslint-env node */
;
const debug = require('./utils/debug');
const fs = require('fs');
const path = require('path');
const CSP_SELF = "'self'";
const CSP_NONE = "'none'";
const CSP_HEADER = 'Content-Security-Policy';
const unique = function (array) {
return array.filter(function (value, index, self) {
return self.indexOf(value) === index;
});
};
const buildPolicyString = function (policyObject) {
return Object.keys(policyObject)
.reduce(function (memo, name) {
var value = policyObject[name];
if (value === null) {
// Override the default value of `'self'`. Instead no entry will be included
// in the CSP. This, in turn, will cause the CSP to fallback to `default-src`
// for this directive. http://www.w3.org/TR/CSP2/#default-src-usage
return memo;
} else {
var sourceList = Array.isArray(value) ? unique(value).join(' ') : value;
return memo + name + ' ' + sourceList + '; ';
}
}, '')
.trim();
};
const getConfigPath = function (projectPkg, projectRoot) {
let configDir = 'config';
if (projectPkg['ember-addon'] && projectPkg['ember-addon']['configPath']) {
configDir = projectPkg['ember-addon']['configPath'];
}
return path.join(projectRoot, configDir, 'content-security-policy.js');
};
/**
* Returns the configuration stored in `config/content-security-policy.js`.
* Returns an empty object if that file does not exist.
*
* @param {string} project
* @param {string} environment
* @return {object}
*/
const readConfig = function (project, environment) {
let configPath = getConfigPath(project.pkg, project.root);
return fs.existsSync(configPath) ? require(configPath)(environment) : {};
};
/**
* Calculates the configuration based on
* - own config (`config/content-security-policy.js`) and
* - run-time config (`config/environment.js`) for legacy support.
*
* @params {string} environment
* @params {object} ownConfig
* @params {object} runConfig
* @params {object} ui
* @returns {object}
*/
function calculateConfig(environment, ownConfig, runConfig, ui) {
let config = {
delivery: ['header'],
enabled: true,
failTests: true,
policy: {
'default-src': [CSP_NONE],
'script-src': [CSP_SELF],
'font-src': [CSP_SELF],
'connect-src': [CSP_SELF],
'img-src': [CSP_SELF],
'style-src': [CSP_SELF],
'media-src': [CSP_SELF],
},
reportOnly: true,
};
ui.writeWarnLine(
'Configuring ember-cli-content-security-policy using `contentSecurityPolicy`, ' +
'`contentSecurityPolicyHeader` and `contentSecurityPolicyMeta` keys in `config/environment.js` ' +
'is deprecate and will be removed in v3.0.0. ember-cli-content-security-policy is now configured ' +
'using `config/content-security-policy.js`. Please find detailed information about this change ' +
'and recommended migration steps in deprecation guide at ' +
'https://github.com/rwjblue/ember-cli-content-security-policy/blob/master/DEPRECATIONS.md.',
!runConfig.contentSecurityPolicy ||
!runConfig.contentSecurityPolicyHeader ||
!runConfig.contentSecurityPolicyMeta
);
// support legacy configuration options
if (runConfig.contentSecurityPolicy) {
// policy object is merged not replaced
Object.assign(config.policy, runConfig.contentSecurityPolicy);
}
if (runConfig.contentSecurityPolicyMeta) {
config.delivery = ['meta'];
}
if (runConfig.contentSecurityPolicyHeader) {
config.reportOnly = runConfig.contentSecurityPolicyHeader !== CSP_HEADER;
}
// apply configuration
Object.assign(config, ownConfig);
return config;
}
/**
* Appends additional directives to an existing policy object.
* It mutates the existing policy object and it's directive values.
*
* If the directive is not defined yet, it's initalized with a copy
* of default-src directive. This is required to not break the built-in
* fallback mechanism of CSP.
*
* If, say, `connect-src` is not defined it will fall back to `default-src`.
* This can cause issues if not respected when extending a given policy
* object. An example:
*
* Developer has has defined the following policy:
* `default-src: 'self' example.com;`
* and an addon appends the connect-src entry live-reload.local the result is:
* `default-src: 'self' example.com; connect-src: live-reload.local;`
*
* After the addons change an xhr to example.com (which was previously permitted,
* via fallback) will now be rejected since it doesn't match live-reload.local.
*
* To mitigate, whenever we append to a non-existing directive we must also copy
* all sources from default-src onto the specified directive.
*
* @param {object} policyObject
* @param {string} directiveName
* @param {string} sourceList
* @return {void}
*/
function appendSourceList(policyObject, directiveName, sourceList) {
let value = policyObject[directiveName];
if (!Array.isArray(value) && value !== undefined && value !== null) {
// null is only supported for legacy reasons
throw new Error(
`Source list must be an array or undefined, ${value} given.`
);
}
if (!Array.isArray(value)) {
// initialize source list with an copy of default-src (see above)
policyObject[directiveName] = policyObject['default-src']
? policyObject['default-src'].slice()
: [];
}
if (policyObject[directiveName].includes("'none'")) {
if (policyObject[directiveName].length > 1) {
throw new Error(
`'none' keyword is exclusive in a CSP directive but source list is ${JSON.stringify(
value
)}`
);
}
policyObject[directiveName] = [];
}
policyObject[directiveName].push(sourceList);
}
module.exports = {
appendSourceList,
buildPolicyString,
calculateConfig,
debug,
readConfig,
};