UNPKG

@mrtc0/csp-html-webpack-plugin

Version:
192 lines (163 loc) 5.56 kB
import * as crypto from "crypto"; import { JSDOM } from "jsdom"; export type DirectiveSet = { [key: string]: Array<string>; }; const strictCspDirectiveSet: DirectiveSet = { "base-uri": [`'self'`], "object-src": [`'none'`], "script-src": [`'strict-dynamic'`], }; export class Csp { private document: Document; private directiveSet: DirectiveSet; constructor(html: string, directiveSet?: DirectiveSet) { this.document = new JSDOM(html).window.document; this.directiveSet = strictCspDirectiveSet; if (directiveSet) { this.directiveSet = directiveSet; } } getDocument(): Document { return this.document; } /** * Refactor all script tags to a single loader script. */ refactorScriptTagsForHashSourceCSP(): void { const scriptTags = this.document.getElementsByTagName("script"); const srcScripts = Array.from(scriptTags).filter((script) => script.hasAttribute("src") ); // remove all sourced scripts srcScripts.forEach((script) => { script.remove(); }); const loaderInlineScript = Csp.generateLoaderScript(srcScripts); const newInlineScript = this.document.createElement("script"); newInlineScript.textContent = loaderInlineScript; this.document.body.appendChild(newInlineScript); } /** * Get the hash of all inline scripts. * @returns The list of hashes of inline scripts. */ getHashAllInlineScripts(): string[] { const hashes: Array<string> = []; const scriptTags = this.document.getElementsByTagName("script"); const inlineScripts = Array.from(scriptTags).filter( (script) => !script.hasAttribute("src") ); inlineScripts.forEach((script) => { const hash = Csp.hashInlineScript(script.textContent || ""); hashes.push(hash); }); return hashes; } /** * Get the hash of all inline styles. * @returns The list of hashes of inline styles. */ getHashAllInlineStyles(): string[] { const hashes: Array<string> = []; const styleTags = this.document.getElementsByTagName("style"); const inlineStyles = Array.from(styleTags); inlineStyles.forEach((style) => { const hash = Csp.hashInlineStyle(style.textContent || ""); hashes.push(hash); }); return hashes; } addCspMetaTag(csp: string): void { const meta = this.document.createElement("meta"); meta.setAttribute("http-equiv", "Content-Security-Policy"); meta.setAttribute("content", csp); this.document.getElementsByTagName("head")[0].prepend(meta); } /** * Generate a CSP directive set. * * @param scriptHashes - The list of hashes of inline scripts. * @param styleHashes - The list of hashes of inline styles. * @returns The CSP directive set. */ generateDirectiveSet(scriptHashes: string[], styleHashes: string[]): string { const autoGeneratedDirectiveSet: DirectiveSet = {}; if (this.directiveSet["script-src"] || scriptHashes.length > 0) { const scriptValues: Set<string> = new Set( [`'strict-dynamic'`].concat( this.directiveSet["script-src"], scriptHashes ) ); autoGeneratedDirectiveSet["script-src"] = Array.from(scriptValues) .flat() .filter((v) => v); } if (this.directiveSet["style-src"] || styleHashes.length > 0) { const styleValues: Set<string> = new Set( [`'self'`].concat(this.directiveSet["style-src"], styleHashes) ); autoGeneratedDirectiveSet["style-src"] = Array.from(styleValues) .flat() .filter((v) => v); } let cspTemplate = { ...this.directiveSet, ...autoGeneratedDirectiveSet }; return Object.entries(cspTemplate) .map(([directive, values]) => { return `${directive} ${values.join(" ")};`; }) .join(""); } /** * Generate loader inline scripts to dynamically load external scripts. * For example: * <script src="https://example.com/analytics.js"></script> * <script src="/app.js"></script> * will be refactored to: * <script> * var scripts = ['https://example.com/analytics.js', '/app.js']; * scripts.forEach(function(scriptUrl) { * var s = document.createElement('script'); * s.src = scriptUrl; * s.async = false; * document.body.appendChild(s); * }); * </script> * * @param scripts - The list of sourced scripts. * @returns The loader script that will load all sourced scripts. */ static generateLoaderScript(scripts: HTMLScriptElement[]): string { if (scripts.length === 0) { return ""; } const srcListFormatted = scripts.map((s) => `'${s.src}'`).join(); return `var scripts = [${srcListFormatted}]; scripts.forEach(function(scriptUrl) { var s = document.createElement('script'); s.src = scriptUrl; s.async = false; document.body.appendChild(s); });\n`; } /** * @param scriptText - The text of the inline script. * @returns The hash of the inline script. */ static hashInlineScript(scriptText: string): string { const hash = crypto .createHash("sha256") .update(scriptText) .digest("base64"); return `'sha256-${hash}'`; } /** * @param styleText - The text of the inline style. * @returns The hash of the inline style. */ static hashInlineStyle(styleText: string): string { const hash = crypto.createHash("sha256").update(styleText).digest("base64"); return `'sha256-${hash}'`; } }