Skip to content

Instantly share code, notes, and snippets.

@cfm
Forked from richie5um/worker.js
Last active July 1, 2023 01:19
Show Gist options
  • Save cfm/a2220bda4486936e17f0f2021814faf0 to your computer and use it in GitHub Desktop.
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
// 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