Skip to content

Instantly share code, notes, and snippets.

@davestewart
Last active July 12, 2023 15:58
Show Gist options
  • Save davestewart/9b8b027caaeec4467ab9900e14b0b5c3 to your computer and use it in GitHub Desktop.
Save davestewart/9b8b027caaeec4467ab9900e14b0b5c3 to your computer and use it in GitHub Desktop.
Content Security Policy builder
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,
})
{
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: *;"
}
// ---------------------------------------------------------------------------------------------------------------------
// 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