Last active
July 12, 2023 15:58
-
-
Save davestewart/9b8b027caaeec4467ab9900e14b0b5c3 to your computer and use it in GitHub Desktop.
Content Security Policy builder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const CSP = require('./csp') | |
// --------------------------------------------------------------------------------------------------------------------- | |
// variables | |
// --------------------------------------------------------------------------------------------------------------------- | |
const csp = `script-src 'self' *; img-src data: *` | |
const isDev = true | |
// --------------------------------------------------------------------------------------------------------------------- | |
// parse | |
// --------------------------------------------------------------------------------------------------------------------- | |
// parse CSP string into camel case object of keyword arrays | |
const parsed1 = CSP.parse(csp) | |
// parse CSP string into kebab case object of strings | |
const parsed2 = CSP.parse(csp, { | |
camel: false, // kebab case | |
strip: false, // keep quotes | |
array: false, // parse to strings | |
}) | |
// --------------------------------------------------------------------------------------------------------------------- | |
// stringify | |
// --------------------------------------------------------------------------------------------------------------------- | |
// create string from single object | |
const str1 = CSP.stringify({ | |
// no need to quote keywords | |
scriptSrc: 'self *', | |
// keyword only-values | |
upgradeInsecureRequests: true, | |
// pass arrays of values | |
imgSrc: [ | |
// 'http://not-needed.com' | |
'data:', | |
'*' | |
], | |
}) | |
// create string from multiple / conditional configs | |
const base = { | |
scriptSrc: 'self *', | |
imgSrc: [ | |
'data:', | |
'*' | |
], | |
} | |
const dev = { | |
scriptSrc: 'http://localhost:8080', | |
} | |
const str2 = CSP.stringify(base, isDev && dev) | |
// log results | |
console.log({ | |
parsed1, | |
parsed2, | |
str1, | |
str2, | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
parsed1: { | |
scriptSrc: [ | |
'self', | |
'*' | |
], | |
imgSrc: [ | |
'data:', | |
'*' | |
] | |
}, | |
parsed2: { | |
'script-src': "'self' *", | |
'img-src': 'data: *' | |
}, | |
str1: "script-src 'self' *; upgrade-insecure-requests;img-src data: *;", | |
str2: "script-src 'self' * http://localhost:8080; img-src data: *;" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// --------------------------------------------------------------------------------------------------------------------- | |
// functions | |
// --------------------------------------------------------------------------------------------------------------------- | |
/** | |
* Convert one or more CSP data structures into a single CSP string | |
* | |
* Features: | |
* | |
* - camelCase property names | |
* - autocompletes property names | |
* - pass values as strings or arrays of strings | |
* - CSP keywords are auto-quoted | |
* - pass one or more configs | |
* | |
* @example: CSP.stringify({ | |
* scriptSrc: 'self *', // keywords are auto-quoted | |
* imgSrc: [ // array of values | |
* 'data:', | |
* '*' | |
* ], | |
* }, isDev && { scriptSrc: 'http://localhost }) | |
* | |
* // "script-src 'self' * http://localhost; img-src data: *" | |
* | |
* @param {CSPData} configs One or more valid CSP Data objects | |
* @return {string} A valid CSP string | |
*/ | |
function stringify (...configs) { | |
const config = configs.length > 1 | |
? merge(...configs) | |
: configs[0] | |
return Object.entries(config).reduce((output, [key, values]) => { | |
// flatten arrays | |
if (Array.isArray(values)) { | |
values = values.flat(100).join(' ') | |
} | |
// split to values | |
values = String(values || '').trim().split(/\s+/) | |
// process values | |
if (values.length) { | |
// auto-quote | |
values = values.map(value => { | |
for (const keyword of keywords) { | |
if (typeof keyword === 'string') { | |
if (value === keyword) { | |
return `'${value}'` | |
} | |
} | |
else if (keyword.test(value)) { | |
return `'${value}'` | |
} | |
} | |
return value | |
}) | |
// unique only | |
values = Array | |
.from(new Set(values)) | |
.join(' ') | |
.trim() | |
// truthy values | |
if (values === 'true' || values === '1') { | |
values = '' | |
} | |
// kebab-case keys | |
key = key | |
.replace(/_/g, '-') | |
.replace(/([a-z])([A-Z])/g, (all, a, b) => `${a}-${b.toLowerCase()}`) | |
// output | |
output += `${key} ${values}; `.replace(' ;', ';') | |
} | |
return output | |
}, '').trim() | |
} | |
/** | |
* Convert a CSP string into a CSP data structure | |
* | |
* @param {string} input The CSP string | |
* @param {CSPOptions} [options] Optional valid parsing options | |
* @returns {CSPData} A CSP data structure | |
*/ | |
function parse (input, options = defaults) { | |
options = Object.assign({}, defaults, options) | |
return input.split(';').reduce((output, part) => { | |
const matches = part.trim().match(/^(\S+)\s+(.+)/) | |
if (matches) { | |
let [, key, value] = matches | |
if (options.camel) { | |
key = key.replace(/-([a-z])/g, (all, a) => a.toUpperCase()) | |
} | |
if (options.strip) { | |
value = value.trim().replace(/'(.+?)'/g, '$1') | |
} | |
if (options.array) { | |
value = value.split(/\s+/) | |
} | |
output[key] = value | |
} | |
return output | |
}, {}) | |
} | |
module.exports = { | |
stringify, | |
parse, | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// helpers | |
// --------------------------------------------------------------------------------------------------------------------- | |
/** | |
* Merge one or more CSP data structures | |
* | |
* Ignores falsy values. Final object is always key:Array | |
* | |
* @param {CSPData|false|null|undefined} configs One or more CSP data structures or strings; falsy values are skipped | |
* @returns {CSPData|string} | |
*/ | |
function merge (...configs) { | |
const output = {} | |
for (let config of configs) { | |
if (config) { | |
if (typeof config === 'string') { | |
config = parse(config) | |
} | |
Object.entries(config).forEach(([key, value]) => { | |
if (!output[key]) { | |
output[key] = [] | |
} | |
if (Array.isArray(value)) { | |
output[key].push(...value) | |
} | |
else { | |
output[key].push(value) | |
} | |
}) | |
} | |
} | |
return output | |
} | |
const keywords = [ | |
'self', | |
'none', | |
'unsafe-inline', | |
'unsafe-eval', | |
'unsafe-hashes', | |
'allow-duplicates', | |
'strict-dynamic', | |
'report-sample', | |
'script', | |
/^sha\d{3}-\w$/, | |
/^nonce-\w$/, | |
] | |
const defaults = { | |
camel: true, | |
strip: true, | |
array: true, | |
} | |
// --------------------------------------------------------------------------------------------------------------------- | |
// types | |
// --------------------------------------------------------------------------------------------------------------------- | |
/** | |
* CSP Data | |
* | |
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy | |
* @see https://content-security-policy.com/ | |
* | |
* Note that keys are camel-cased! | |
* | |
* @typedef {Object} CSPData | |
* | |
* Source values: | |
* | |
* @property {CSPValue} [defaultSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src | |
* @property {CSPValue} [childSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/child-src | |
* @property {CSPValue} [connectSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src | |
* @property {CSPValue} [fontSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/font-src | |
* @property {CSPValue} [frameSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src | |
* @property {CSPValue} [imgSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/img-src | |
* @property {CSPValue} [manifestSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/manifest-src | |
* @property {CSPValue} [mediaSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/media-src | |
* @property {CSPValue} [objectSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/object-src | |
* @property {CSPValue} [prefetchSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/prefetch-src | |
* @property {CSPValue} [scriptSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src | |
* @property {CSPValue} [scriptSrcElem] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src-elem | |
* @property {CSPValue} [scriptSrcAttr] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src-attr | |
* @property {CSPValue} [styleSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src | |
* @property {CSPValue} [styleSrcElem] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src-elem | |
* @property {CSPValue} [styleSrcAttr] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src-attr | |
* @property {CSPValue} [workerSrc] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/worker-src | |
* | |
* Other: | |
* | |
* @property {CSPValue} [baseUri] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/base-uri | |
* @property {CSPValue} [formAction] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/form-action | |
* @property {CSPValue} [frameAncestors] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors | |
* @property {CSPValue} [reportTo] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to | |
* @property {CSPValue} [requireTrustedTypesFor] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for | |
* @property {CSPValue} [sandbox] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox | |
* @property {CSPValue} [trustedTypes] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types | |
* @property {CSPValue} [upgradeInsecureRequests] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/upgrade-insecure-requests | |
*/ | |
/** | |
* Valid CSP values | |
* | |
* @typedef {(string|string[])} CSPValue | |
*/ | |
/** | |
* CSP parsing options | |
* | |
* @typedef {Object} CSPOptions | |
* @property {boolean} [camel] Convert directive names to camel case; defaults to true | |
* @property {boolean} [strip] Strip directive keywords of quotes; defaults to true | |
* @property {boolean} [array] Parse directive values into arrays; defaults to true | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment