-
-
Save cfm/a2220bda4486936e17f0f2021814faf0 to your computer and use it in GitHub Desktop.
Cloudflare Worker script to apply a dynamic Content-Security-Policy header for each fetch request
This file contains hidden or 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
// Cloudflare Worker script to apply a dynamic Content-Security-Policy header | |
// for each fetch request by: | |
// | |
// 1. generating a per-request nonce; | |
// | |
// 2. injecting it into the "nonce" attribute on all SCRIPT and STYLE elements; | |
// and | |
// | |
// 3. adding a Content-Security-Policy allowing that nonce in the "script-src" | |
// and "style-src" attributes. | |
addEventListener("fetch", (event) => { | |
return event.respondWith(injectCspNonce(event.request)); | |
}); | |
async function injectCspNonce(req) { | |
let response = await fetch(req); | |
let headers = new Headers(response.headers); | |
// Return the response unmodified if it's not HTML. | |
if ( | |
headers.has("Content-Type") && | |
!headers.get("Content-Type").includes("text/html") | |
) { | |
return new Response(response.body, { | |
status: response.status, | |
statusText: response.statusText, | |
headers: headers, | |
}); | |
} | |
// Generate the per-request nonce and generate a Content-Security-Policy | |
// header that allows it. | |
let nonce = btoa(crypto.getRandomValues(new Uint32Array(2))); | |
headers.set("Content-Security-Policy", generateCspString(cspTemplate, nonce)); | |
// Inject the nonce into all SCRIPT and STYLE elements. | |
// | |
// CONFIGURATION: You can chain any series of HTMLRewriter.on() calls, one | |
// for each element that should receive a "nonce" attribute. | |
const rewriter = new HTMLRewriter() | |
.on("script", new AttributeRewriter("nonce", "", nonce)) | |
.on("style", new AttributeRewriter("nonce", "", nonce)); | |
return rewriter.transform( | |
new Response(response.body, { | |
status: response.status, | |
statusText: response.statusText, | |
headers: headers, | |
}) | |
); | |
} | |
// The template of (directive, source[]) pairs from which the final | |
// Content-Security-Policy header will be rendered. "{{nonce}}" in any source | |
// value will be replaced with the per-request generated nonce. | |
// | |
// CONFIGURATION: You can customize these directives. | |
const cspTemplate = { | |
"default-src": ["'none'"], | |
"script-src": ["'self'", "{{nonce}}"], | |
"style-src": ["'self'", "{{nonce}}"], | |
"img-src": ["'self'"], | |
"font-src": ["'self'"], | |
}; | |
// --- HELPERS --- | |
// Build the value of the Content-Security-Policy header from the template, | |
// interpolating the per-request generated nonce. | |
function generateCspString(cspTemplate, nonce) { | |
let directives = []; | |
Object.keys(cspTemplate).map(function (key, index) { | |
let values = cspTemplate[key].map(function (value) { | |
if (value === "{{nonce}}") { | |
return (value = `'nonce-${nonce}'`); | |
} | |
return value; | |
}); | |
let directive = `${key} ${values.join(" ")}`; | |
directives.push(directive); | |
}); | |
return directives.join("; "); | |
} | |
// Rewrite the named attribute from the find-value to the replace-value. | |
class AttributeRewriter { | |
constructor(name, find, replace) { | |
this.name = name; | |
this.find = find; | |
this.replace = replace; | |
} | |
element(element) { | |
const attribute = element.getAttribute(this.name); | |
if (this.find) { | |
element.setAttribute( | |
this.name, | |
attribute.replace(this.find, this.replace) | |
); | |
} else { | |
element.setAttribute(this.name, this.replace); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment