UNPKG

@mrtc0/csp-html-webpack-plugin

Version:
175 lines (174 loc) 6.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Csp = void 0; const crypto = __importStar(require("crypto")); const jsdom_1 = require("jsdom"); const strictCspDirectiveSet = { "base-uri": [`'self'`], "object-src": [`'none'`], "script-src": [`'strict-dynamic'`], }; class Csp { constructor(html, directiveSet) { this.document = new jsdom_1.JSDOM(html).window.document; this.directiveSet = strictCspDirectiveSet; if (directiveSet) { this.directiveSet = directiveSet; } } getDocument() { return this.document; } /** * Refactor all script tags to a single loader script. */ refactorScriptTagsForHashSourceCSP() { 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() { const hashes = []; 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() { const hashes = []; 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) { 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, styleHashes) { const autoGeneratedDirectiveSet = {}; if (this.directiveSet["script-src"] || scriptHashes.length > 0) { const scriptValues = 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 = new Set([`'self'`].concat(this.directiveSet["style-src"], styleHashes)); autoGeneratedDirectiveSet["style-src"] = Array.from(styleValues) .flat() .filter((v) => v); } let cspTemplate = Object.assign(Object.assign({}, 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) { 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) { 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) { const hash = crypto.createHash("sha256").update(styleText).digest("base64"); return `'sha256-${hash}'`; } } exports.Csp = Csp;