@mrtc0/csp-html-webpack-plugin
Version:
175 lines (174 loc) • 6.7 kB
JavaScript
;
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;