@elliemae/pui-cli 8.40.3 → 8.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/server/middlewares.js +0 -9
- package/dist/cjs/webpack/csp-plugin.js +83 -0
- package/dist/cjs/webpack/csp.js +158 -0
- package/dist/cjs/webpack/webpack.dev.babel.js +3 -0
- package/dist/cjs/webpack/webpack.prod.babel.js +4 -1
- package/dist/esm/server/middlewares.js +0 -9
- package/dist/esm/webpack/csp-plugin.js +53 -0
- package/dist/esm/webpack/csp.js +128 -0
- package/dist/esm/webpack/webpack.dev.babel.js +3 -0
- package/dist/esm/webpack/webpack.prod.babel.js +4 -1
- package/dist/types/lib/webpack/csp-plugin.d.ts +34 -0
- package/dist/types/lib/webpack/csp.d.ts +65 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +28 -27
|
@@ -37,7 +37,6 @@ var import_cors = __toESM(require("cors"), 1);
|
|
|
37
37
|
var import_compression = __toESM(require("compression"), 1);
|
|
38
38
|
var import_express_static_gzip = __toESM(require("express-static-gzip"), 1);
|
|
39
39
|
var import_pino_http = __toESM(require("pino-http"), 1);
|
|
40
|
-
var import_csp = require("./csp.js");
|
|
41
40
|
var import_helpers = require("../webpack/helpers.js");
|
|
42
41
|
const paths = (0, import_helpers.getPaths)();
|
|
43
42
|
const setupDefaultMiddlewares = (app) => {
|
|
@@ -53,7 +52,6 @@ const setupDefaultMiddlewares = (app) => {
|
|
|
53
52
|
app.use(pino);
|
|
54
53
|
app.use((0, import_cors.default)());
|
|
55
54
|
app.options("*", (0, import_cors.default)());
|
|
56
|
-
(0, import_csp.csp)(app);
|
|
57
55
|
app.use(import_express.default.urlencoded({ extended: false }));
|
|
58
56
|
app.use(import_express.default.text({ type: "text/plain" }));
|
|
59
57
|
app.use(import_express.default.json({ type: "application/json" }));
|
|
@@ -61,9 +59,6 @@ const setupDefaultMiddlewares = (app) => {
|
|
|
61
59
|
const setupAdditionalMiddlewars = (app, options = {}) => {
|
|
62
60
|
const { buildPath = paths.buildPath, basePath = paths.basePath } = options;
|
|
63
61
|
app.use((0, import_compression.default)());
|
|
64
|
-
app.get(basePath, (req, res) => {
|
|
65
|
-
(0, import_csp.sendFileWithCSPNonce)({ buildPath, res });
|
|
66
|
-
});
|
|
67
62
|
app.use(
|
|
68
63
|
basePath,
|
|
69
64
|
(0, import_express_static_gzip.default)(buildPath, {
|
|
@@ -73,9 +68,5 @@ const setupAdditionalMiddlewars = (app, options = {}) => {
|
|
|
73
68
|
})
|
|
74
69
|
);
|
|
75
70
|
app.use((0, import_express_static_gzip.default)("cdn", {}));
|
|
76
|
-
app.get(
|
|
77
|
-
"*",
|
|
78
|
-
(req, res) => (0, import_csp.sendFileWithCSPNonce)({ buildPath, res })
|
|
79
|
-
);
|
|
80
71
|
return app;
|
|
81
72
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var csp_plugin_exports = {};
|
|
30
|
+
__export(csp_plugin_exports, {
|
|
31
|
+
CspPlugin: () => CspPlugin
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(csp_plugin_exports);
|
|
34
|
+
var import_html_webpack_plugin = __toESM(require("html-webpack-plugin"), 1);
|
|
35
|
+
var import_csp = require("./csp.js");
|
|
36
|
+
const defaultOptions = {
|
|
37
|
+
enableUnsafeEval: false
|
|
38
|
+
};
|
|
39
|
+
class CspPlugin {
|
|
40
|
+
#options = defaultOptions;
|
|
41
|
+
#htmlWebpackPlugin;
|
|
42
|
+
/**
|
|
43
|
+
*
|
|
44
|
+
* @param htmlWebpackPlugin
|
|
45
|
+
* @param {object} options Additional options for this module.
|
|
46
|
+
*/
|
|
47
|
+
constructor(htmlWebpackPlugin, options) {
|
|
48
|
+
this.#htmlWebpackPlugin = htmlWebpackPlugin;
|
|
49
|
+
this.#options = { ...this.#options, ...options ?? {} };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Processes HtmlWebpackPlugin's html data by adding the CSP
|
|
53
|
+
* @param compilation
|
|
54
|
+
* @param htmlPluginData
|
|
55
|
+
* @param htmlPluginData.html
|
|
56
|
+
* @param compileCb
|
|
57
|
+
* @returns {*}
|
|
58
|
+
*/
|
|
59
|
+
processCsp(compilation, htmlPluginData, compileCb) {
|
|
60
|
+
const cspModule = new import_csp.CSP(htmlPluginData.html);
|
|
61
|
+
cspModule.refactorSourcedScriptsForHashBasedCsp();
|
|
62
|
+
const scriptHashes = cspModule.hashAllInlineScripts();
|
|
63
|
+
const { enableUnsafeEval } = this.#options;
|
|
64
|
+
const strictCsp = import_csp.CSP.getStrictCsp(scriptHashes, {
|
|
65
|
+
enableUnsafeEval
|
|
66
|
+
});
|
|
67
|
+
cspModule.addMetaTag(strictCsp);
|
|
68
|
+
htmlPluginData.html = cspModule.serializeDom();
|
|
69
|
+
return compileCb(null, htmlPluginData);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Hooks into webpack to collect assets and hash them, build the policy, and add it into our HTML template
|
|
73
|
+
* @param compiler
|
|
74
|
+
*/
|
|
75
|
+
apply(compiler) {
|
|
76
|
+
compiler.hooks.compilation.tap("CspPlugin", (compilation) => {
|
|
77
|
+
import_html_webpack_plugin.default.getCompilationHooks(compilation).beforeEmit.tapAsync(
|
|
78
|
+
"CspPlugin",
|
|
79
|
+
this.processCsp.bind(this, compilation)
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var csp_exports = {};
|
|
30
|
+
__export(csp_exports, {
|
|
31
|
+
CSP: () => CSP
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(csp_exports);
|
|
34
|
+
var crypto = __toESM(require("node:crypto"), 1);
|
|
35
|
+
var cheerio = __toESM(require("cheerio"), 1);
|
|
36
|
+
class CSP {
|
|
37
|
+
static HASH_FUNCTION = "sha256";
|
|
38
|
+
static INLINE_SCRIPT_SELECTOR = "script:not([src])";
|
|
39
|
+
static SOURCED_SCRIPT_SELECTOR = "script[src]";
|
|
40
|
+
$;
|
|
41
|
+
constructor(html) {
|
|
42
|
+
this.$ = cheerio.load(html);
|
|
43
|
+
}
|
|
44
|
+
serializeDom() {
|
|
45
|
+
return this.$.root().html() ?? "";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns a strict Content Security Policy for mittigating XSS.
|
|
49
|
+
* For more details read csp.withgoogle.com.
|
|
50
|
+
* If you modify this CSP, make sure it has not become trivially bypassable by
|
|
51
|
+
* checking the policy using csp-evaluator.withgoogle.com.
|
|
52
|
+
* @param hashes A list of sha-256 hashes of trusted inline scripts.
|
|
53
|
+
* @param cspOptions
|
|
54
|
+
* @param enableTrustedTypes If Trusted Types should be enabled for scripts.
|
|
55
|
+
* @param enableBrowserFallbacks If fallbacks for older browsers should be
|
|
56
|
+
* added. This is will not weaken the policy as modern browsers will ignore
|
|
57
|
+
* the fallbacks.
|
|
58
|
+
* @param enableUnsafeEval If you cannot remove all uses of eval(), you can
|
|
59
|
+
* still set a strict CSP, but you will have to use the 'unsafe-eval'
|
|
60
|
+
* keyword which will make your policy slightly less secure.
|
|
61
|
+
* @param cspOptions.enableBrowserFallbacks
|
|
62
|
+
* @param cspOptions.enableTrustedTypes
|
|
63
|
+
* @param cspOptions.enableUnsafeEval
|
|
64
|
+
* @returns A strict Content Security Policy string.
|
|
65
|
+
*/
|
|
66
|
+
static getStrictCsp(hashes, cspOptions = {
|
|
67
|
+
enableUnsafeEval: false
|
|
68
|
+
}) {
|
|
69
|
+
const strictCspTemplate = {
|
|
70
|
+
// 'strict-dynamic' allows hashed scripts to create new scripts.
|
|
71
|
+
"script-src": [`'strict-dynamic'`, ...hashes ?? []],
|
|
72
|
+
// Restricts `object-src` to disable dangerous plugins like Flash.
|
|
73
|
+
"object-src": [`'none'`],
|
|
74
|
+
// Restricts `base-uri` to block the injection of `<base>` tags. This
|
|
75
|
+
// prevents attackers from changing the locations of scripts loaded from
|
|
76
|
+
// relative URLs.
|
|
77
|
+
"base-uri": [`'self'`]
|
|
78
|
+
};
|
|
79
|
+
if (cspOptions.enableUnsafeEval) {
|
|
80
|
+
strictCspTemplate["script-src"].push(`'unsafe-eval'`);
|
|
81
|
+
}
|
|
82
|
+
return Object.entries(strictCspTemplate).map(([directive, values]) => `${directive} ${values.join(" ")};`).join("");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Enables a CSP via a meta tag at the beginning of the document.
|
|
86
|
+
* Warning: It's recommended to set CSP as HTTP response header instead of
|
|
87
|
+
* using a meta tag. Injections before the meta tag will not be covered by CSP
|
|
88
|
+
* and meta tags don't support CSP in report-only mode.
|
|
89
|
+
* @param csp A Content Security Policy string.
|
|
90
|
+
*/
|
|
91
|
+
addMetaTag(csp) {
|
|
92
|
+
let metaTag = this.$('meta[http-equiv="Content-Security-Policy"]');
|
|
93
|
+
if (!metaTag.length) {
|
|
94
|
+
metaTag = cheerio.load('<meta http-equiv="Content-Security-Policy">')(
|
|
95
|
+
"meta"
|
|
96
|
+
);
|
|
97
|
+
metaTag.prependTo(this.$("head"));
|
|
98
|
+
}
|
|
99
|
+
metaTag.attr("content", csp);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Replaces all sourced scripts with a single inline script that can be hashed
|
|
103
|
+
*/
|
|
104
|
+
refactorSourcedScriptsForHashBasedCsp() {
|
|
105
|
+
const scriptInfoList = this.$(CSP.SOURCED_SCRIPT_SELECTOR).map((i, script) => {
|
|
106
|
+
const src = this.$(script).attr("src") ?? "";
|
|
107
|
+
const type = this.$(script).attr("type") ?? "";
|
|
108
|
+
this.$(script).remove();
|
|
109
|
+
return { src, type };
|
|
110
|
+
}).toArray().filter((info) => info.src);
|
|
111
|
+
const loaderScript = CSP.createLoaderScript(scriptInfoList);
|
|
112
|
+
if (!loaderScript) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const newScript = cheerio.load("<script>")("script");
|
|
116
|
+
newScript.text(loaderScript);
|
|
117
|
+
newScript.appendTo(this.$("body"));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Returns a list of hashes of all inline scripts found in the HTML document.
|
|
121
|
+
* @returns A list of sha-256 hashes of inline scripts.
|
|
122
|
+
*/
|
|
123
|
+
hashAllInlineScripts() {
|
|
124
|
+
return this.$(CSP.INLINE_SCRIPT_SELECTOR).map((i, elem) => CSP.hashInlineScript(this.$(elem).html() ?? "")).get();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Returns JS code for dynamically loading sourced (external) scripts.
|
|
128
|
+
* @param scriptInfoList A list of objects containing src and type for scripts that should be loaded
|
|
129
|
+
* @returns JS code for loading scripts.
|
|
130
|
+
*/
|
|
131
|
+
static createLoaderScript(scriptInfoList) {
|
|
132
|
+
if (!scriptInfoList.length) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
return `
|
|
136
|
+
var scripts = ${JSON.stringify(scriptInfoList)};
|
|
137
|
+
scripts.forEach(function(scriptInfo) {
|
|
138
|
+
var s = document.createElement('script');
|
|
139
|
+
s.src = scriptInfo.src;
|
|
140
|
+
if (scriptInfo.type) {
|
|
141
|
+
s.type = scriptInfo.type;
|
|
142
|
+
}
|
|
143
|
+
s.async = false; // preserve execution order.
|
|
144
|
+
document.body.appendChild(s);
|
|
145
|
+
});
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Calculates a CSP compatible hash of an inline script.
|
|
150
|
+
* @param scriptText Text between opening and closing script tag. Has to
|
|
151
|
+
* include whitespaces and newlines!
|
|
152
|
+
* @returns A sha-256 hash of the script.
|
|
153
|
+
*/
|
|
154
|
+
static hashInlineScript(scriptText) {
|
|
155
|
+
const hash = crypto.createHash(CSP.HASH_FUNCTION).update(scriptText, "utf-8").digest("base64");
|
|
156
|
+
return `'${CSP.HASH_FUNCTION}-${hash}'`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -114,6 +114,9 @@ const devConfig = {
|
|
|
114
114
|
googleTagManager: (0, import_helpers.isGoogleTagManagerEnabled)()
|
|
115
115
|
}
|
|
116
116
|
}),
|
|
117
|
+
// new CspPlugin(HtmlWebpackPlugin, {
|
|
118
|
+
// enableUnsafeEval: true,
|
|
119
|
+
// }),
|
|
117
120
|
new import_circular_dependency_plugin.default({
|
|
118
121
|
exclude: /a\.(js|ts|jsx|tsx)|node_modules/,
|
|
119
122
|
// exclude node_modules
|
|
@@ -57,6 +57,7 @@ const getProdConfig = ({ latestVersion = true } = {}) => {
|
|
|
57
57
|
},
|
|
58
58
|
optimization: {
|
|
59
59
|
moduleIds: "deterministic",
|
|
60
|
+
realContentHash: false,
|
|
60
61
|
minimizer: [
|
|
61
62
|
new import_esbuild_loader.EsbuildPlugin({
|
|
62
63
|
target: (0, import_browserslist_to_esbuild.default)(),
|
|
@@ -140,5 +141,7 @@ const htmlWebpackPlugin = new import_html_webpack_plugin.default({
|
|
|
140
141
|
}
|
|
141
142
|
});
|
|
142
143
|
const config = (0, import_webpack_base_babel.baseConfig)(getProdConfig());
|
|
143
|
-
if (config.plugins)
|
|
144
|
+
if (config.plugins) {
|
|
145
|
+
config.plugins.push(htmlWebpackPlugin);
|
|
146
|
+
}
|
|
144
147
|
var webpack_prod_babel_default = config;
|
|
@@ -3,7 +3,6 @@ import cors from "cors";
|
|
|
3
3
|
import compression from "compression";
|
|
4
4
|
import expressStaticGzip from "express-static-gzip";
|
|
5
5
|
import pinoLogger from "pino-http";
|
|
6
|
-
import { csp, sendFileWithCSPNonce } from "./csp.js";
|
|
7
6
|
import { getPaths } from "../webpack/helpers.js";
|
|
8
7
|
const paths = getPaths();
|
|
9
8
|
const setupDefaultMiddlewares = (app) => {
|
|
@@ -19,7 +18,6 @@ const setupDefaultMiddlewares = (app) => {
|
|
|
19
18
|
app.use(pino);
|
|
20
19
|
app.use(cors());
|
|
21
20
|
app.options("*", cors());
|
|
22
|
-
csp(app);
|
|
23
21
|
app.use(express.urlencoded({ extended: false }));
|
|
24
22
|
app.use(express.text({ type: "text/plain" }));
|
|
25
23
|
app.use(express.json({ type: "application/json" }));
|
|
@@ -27,9 +25,6 @@ const setupDefaultMiddlewares = (app) => {
|
|
|
27
25
|
const setupAdditionalMiddlewars = (app, options = {}) => {
|
|
28
26
|
const { buildPath = paths.buildPath, basePath = paths.basePath } = options;
|
|
29
27
|
app.use(compression());
|
|
30
|
-
app.get(basePath, (req, res) => {
|
|
31
|
-
sendFileWithCSPNonce({ buildPath, res });
|
|
32
|
-
});
|
|
33
28
|
app.use(
|
|
34
29
|
basePath,
|
|
35
30
|
expressStaticGzip(buildPath, {
|
|
@@ -39,10 +34,6 @@ const setupAdditionalMiddlewars = (app, options = {}) => {
|
|
|
39
34
|
})
|
|
40
35
|
);
|
|
41
36
|
app.use(expressStaticGzip("cdn", {}));
|
|
42
|
-
app.get(
|
|
43
|
-
"*",
|
|
44
|
-
(req, res) => sendFileWithCSPNonce({ buildPath, res })
|
|
45
|
-
);
|
|
46
37
|
return app;
|
|
47
38
|
};
|
|
48
39
|
export {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import HtmlWebpackPlugin from "html-webpack-plugin";
|
|
2
|
+
import { CSP } from "./csp.js";
|
|
3
|
+
const defaultOptions = {
|
|
4
|
+
enableUnsafeEval: false
|
|
5
|
+
};
|
|
6
|
+
class CspPlugin {
|
|
7
|
+
#options = defaultOptions;
|
|
8
|
+
#htmlWebpackPlugin;
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* @param htmlWebpackPlugin
|
|
12
|
+
* @param {object} options Additional options for this module.
|
|
13
|
+
*/
|
|
14
|
+
constructor(htmlWebpackPlugin, options) {
|
|
15
|
+
this.#htmlWebpackPlugin = htmlWebpackPlugin;
|
|
16
|
+
this.#options = { ...this.#options, ...options ?? {} };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Processes HtmlWebpackPlugin's html data by adding the CSP
|
|
20
|
+
* @param compilation
|
|
21
|
+
* @param htmlPluginData
|
|
22
|
+
* @param htmlPluginData.html
|
|
23
|
+
* @param compileCb
|
|
24
|
+
* @returns {*}
|
|
25
|
+
*/
|
|
26
|
+
processCsp(compilation, htmlPluginData, compileCb) {
|
|
27
|
+
const cspModule = new CSP(htmlPluginData.html);
|
|
28
|
+
cspModule.refactorSourcedScriptsForHashBasedCsp();
|
|
29
|
+
const scriptHashes = cspModule.hashAllInlineScripts();
|
|
30
|
+
const { enableUnsafeEval } = this.#options;
|
|
31
|
+
const strictCsp = CSP.getStrictCsp(scriptHashes, {
|
|
32
|
+
enableUnsafeEval
|
|
33
|
+
});
|
|
34
|
+
cspModule.addMetaTag(strictCsp);
|
|
35
|
+
htmlPluginData.html = cspModule.serializeDom();
|
|
36
|
+
return compileCb(null, htmlPluginData);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Hooks into webpack to collect assets and hash them, build the policy, and add it into our HTML template
|
|
40
|
+
* @param compiler
|
|
41
|
+
*/
|
|
42
|
+
apply(compiler) {
|
|
43
|
+
compiler.hooks.compilation.tap("CspPlugin", (compilation) => {
|
|
44
|
+
HtmlWebpackPlugin.getCompilationHooks(compilation).beforeEmit.tapAsync(
|
|
45
|
+
"CspPlugin",
|
|
46
|
+
this.processCsp.bind(this, compilation)
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
CspPlugin
|
|
53
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as cheerio from "cheerio";
|
|
3
|
+
class CSP {
|
|
4
|
+
static HASH_FUNCTION = "sha256";
|
|
5
|
+
static INLINE_SCRIPT_SELECTOR = "script:not([src])";
|
|
6
|
+
static SOURCED_SCRIPT_SELECTOR = "script[src]";
|
|
7
|
+
$;
|
|
8
|
+
constructor(html) {
|
|
9
|
+
this.$ = cheerio.load(html);
|
|
10
|
+
}
|
|
11
|
+
serializeDom() {
|
|
12
|
+
return this.$.root().html() ?? "";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns a strict Content Security Policy for mittigating XSS.
|
|
16
|
+
* For more details read csp.withgoogle.com.
|
|
17
|
+
* If you modify this CSP, make sure it has not become trivially bypassable by
|
|
18
|
+
* checking the policy using csp-evaluator.withgoogle.com.
|
|
19
|
+
* @param hashes A list of sha-256 hashes of trusted inline scripts.
|
|
20
|
+
* @param cspOptions
|
|
21
|
+
* @param enableTrustedTypes If Trusted Types should be enabled for scripts.
|
|
22
|
+
* @param enableBrowserFallbacks If fallbacks for older browsers should be
|
|
23
|
+
* added. This is will not weaken the policy as modern browsers will ignore
|
|
24
|
+
* the fallbacks.
|
|
25
|
+
* @param enableUnsafeEval If you cannot remove all uses of eval(), you can
|
|
26
|
+
* still set a strict CSP, but you will have to use the 'unsafe-eval'
|
|
27
|
+
* keyword which will make your policy slightly less secure.
|
|
28
|
+
* @param cspOptions.enableBrowserFallbacks
|
|
29
|
+
* @param cspOptions.enableTrustedTypes
|
|
30
|
+
* @param cspOptions.enableUnsafeEval
|
|
31
|
+
* @returns A strict Content Security Policy string.
|
|
32
|
+
*/
|
|
33
|
+
static getStrictCsp(hashes, cspOptions = {
|
|
34
|
+
enableUnsafeEval: false
|
|
35
|
+
}) {
|
|
36
|
+
const strictCspTemplate = {
|
|
37
|
+
// 'strict-dynamic' allows hashed scripts to create new scripts.
|
|
38
|
+
"script-src": [`'strict-dynamic'`, ...hashes ?? []],
|
|
39
|
+
// Restricts `object-src` to disable dangerous plugins like Flash.
|
|
40
|
+
"object-src": [`'none'`],
|
|
41
|
+
// Restricts `base-uri` to block the injection of `<base>` tags. This
|
|
42
|
+
// prevents attackers from changing the locations of scripts loaded from
|
|
43
|
+
// relative URLs.
|
|
44
|
+
"base-uri": [`'self'`]
|
|
45
|
+
};
|
|
46
|
+
if (cspOptions.enableUnsafeEval) {
|
|
47
|
+
strictCspTemplate["script-src"].push(`'unsafe-eval'`);
|
|
48
|
+
}
|
|
49
|
+
return Object.entries(strictCspTemplate).map(([directive, values]) => `${directive} ${values.join(" ")};`).join("");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Enables a CSP via a meta tag at the beginning of the document.
|
|
53
|
+
* Warning: It's recommended to set CSP as HTTP response header instead of
|
|
54
|
+
* using a meta tag. Injections before the meta tag will not be covered by CSP
|
|
55
|
+
* and meta tags don't support CSP in report-only mode.
|
|
56
|
+
* @param csp A Content Security Policy string.
|
|
57
|
+
*/
|
|
58
|
+
addMetaTag(csp) {
|
|
59
|
+
let metaTag = this.$('meta[http-equiv="Content-Security-Policy"]');
|
|
60
|
+
if (!metaTag.length) {
|
|
61
|
+
metaTag = cheerio.load('<meta http-equiv="Content-Security-Policy">')(
|
|
62
|
+
"meta"
|
|
63
|
+
);
|
|
64
|
+
metaTag.prependTo(this.$("head"));
|
|
65
|
+
}
|
|
66
|
+
metaTag.attr("content", csp);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Replaces all sourced scripts with a single inline script that can be hashed
|
|
70
|
+
*/
|
|
71
|
+
refactorSourcedScriptsForHashBasedCsp() {
|
|
72
|
+
const scriptInfoList = this.$(CSP.SOURCED_SCRIPT_SELECTOR).map((i, script) => {
|
|
73
|
+
const src = this.$(script).attr("src") ?? "";
|
|
74
|
+
const type = this.$(script).attr("type") ?? "";
|
|
75
|
+
this.$(script).remove();
|
|
76
|
+
return { src, type };
|
|
77
|
+
}).toArray().filter((info) => info.src);
|
|
78
|
+
const loaderScript = CSP.createLoaderScript(scriptInfoList);
|
|
79
|
+
if (!loaderScript) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const newScript = cheerio.load("<script>")("script");
|
|
83
|
+
newScript.text(loaderScript);
|
|
84
|
+
newScript.appendTo(this.$("body"));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Returns a list of hashes of all inline scripts found in the HTML document.
|
|
88
|
+
* @returns A list of sha-256 hashes of inline scripts.
|
|
89
|
+
*/
|
|
90
|
+
hashAllInlineScripts() {
|
|
91
|
+
return this.$(CSP.INLINE_SCRIPT_SELECTOR).map((i, elem) => CSP.hashInlineScript(this.$(elem).html() ?? "")).get();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns JS code for dynamically loading sourced (external) scripts.
|
|
95
|
+
* @param scriptInfoList A list of objects containing src and type for scripts that should be loaded
|
|
96
|
+
* @returns JS code for loading scripts.
|
|
97
|
+
*/
|
|
98
|
+
static createLoaderScript(scriptInfoList) {
|
|
99
|
+
if (!scriptInfoList.length) {
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
return `
|
|
103
|
+
var scripts = ${JSON.stringify(scriptInfoList)};
|
|
104
|
+
scripts.forEach(function(scriptInfo) {
|
|
105
|
+
var s = document.createElement('script');
|
|
106
|
+
s.src = scriptInfo.src;
|
|
107
|
+
if (scriptInfo.type) {
|
|
108
|
+
s.type = scriptInfo.type;
|
|
109
|
+
}
|
|
110
|
+
s.async = false; // preserve execution order.
|
|
111
|
+
document.body.appendChild(s);
|
|
112
|
+
});
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Calculates a CSP compatible hash of an inline script.
|
|
117
|
+
* @param scriptText Text between opening and closing script tag. Has to
|
|
118
|
+
* include whitespaces and newlines!
|
|
119
|
+
* @returns A sha-256 hash of the script.
|
|
120
|
+
*/
|
|
121
|
+
static hashInlineScript(scriptText) {
|
|
122
|
+
const hash = crypto.createHash(CSP.HASH_FUNCTION).update(scriptText, "utf-8").digest("base64");
|
|
123
|
+
return `'${CSP.HASH_FUNCTION}-${hash}'`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
CSP
|
|
128
|
+
};
|
|
@@ -84,6 +84,9 @@ const devConfig = {
|
|
|
84
84
|
googleTagManager: isGoogleTagManagerEnabled()
|
|
85
85
|
}
|
|
86
86
|
}),
|
|
87
|
+
// new CspPlugin(HtmlWebpackPlugin, {
|
|
88
|
+
// enableUnsafeEval: true,
|
|
89
|
+
// }),
|
|
87
90
|
new CircularDependencyPlugin({
|
|
88
91
|
exclude: /a\.(js|ts|jsx|tsx)|node_modules/,
|
|
89
92
|
// exclude node_modules
|
|
@@ -29,6 +29,7 @@ const getProdConfig = ({ latestVersion = true } = {}) => {
|
|
|
29
29
|
},
|
|
30
30
|
optimization: {
|
|
31
31
|
moduleIds: "deterministic",
|
|
32
|
+
realContentHash: false,
|
|
32
33
|
minimizer: [
|
|
33
34
|
new EsbuildPlugin({
|
|
34
35
|
target: browserslistToEsbuild(),
|
|
@@ -112,7 +113,9 @@ const htmlWebpackPlugin = new HtmlWebpackPlugin({
|
|
|
112
113
|
}
|
|
113
114
|
});
|
|
114
115
|
const config = baseConfig(getProdConfig());
|
|
115
|
-
if (config.plugins)
|
|
116
|
+
if (config.plugins) {
|
|
117
|
+
config.plugins.push(htmlWebpackPlugin);
|
|
118
|
+
}
|
|
116
119
|
var webpack_prod_babel_default = config;
|
|
117
120
|
export {
|
|
118
121
|
webpack_prod_babel_default as default
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import webpack from 'webpack';
|
|
2
|
+
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
|
3
|
+
type HTMLWebpackPluginData = {
|
|
4
|
+
html: string;
|
|
5
|
+
outputName: string;
|
|
6
|
+
plugin: HtmlWebpackPlugin;
|
|
7
|
+
};
|
|
8
|
+
declare const defaultOptions: {
|
|
9
|
+
enableUnsafeEval: boolean;
|
|
10
|
+
};
|
|
11
|
+
declare class CspPlugin {
|
|
12
|
+
#private;
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* @param htmlWebpackPlugin
|
|
16
|
+
* @param {object} options Additional options for this module.
|
|
17
|
+
*/
|
|
18
|
+
constructor(htmlWebpackPlugin: typeof HtmlWebpackPlugin, options?: typeof defaultOptions);
|
|
19
|
+
/**
|
|
20
|
+
* Processes HtmlWebpackPlugin's html data by adding the CSP
|
|
21
|
+
* @param compilation
|
|
22
|
+
* @param htmlPluginData
|
|
23
|
+
* @param htmlPluginData.html
|
|
24
|
+
* @param compileCb
|
|
25
|
+
* @returns {*}
|
|
26
|
+
*/
|
|
27
|
+
processCsp(compilation: webpack.Compilation, htmlPluginData: HTMLWebpackPluginData, compileCb: (err: Error | null, htmlPluginData: HTMLWebpackPluginData) => void): void;
|
|
28
|
+
/**
|
|
29
|
+
* Hooks into webpack to collect assets and hash them, build the policy, and add it into our HTML template
|
|
30
|
+
* @param compiler
|
|
31
|
+
*/
|
|
32
|
+
apply(compiler: webpack.Compiler): void;
|
|
33
|
+
}
|
|
34
|
+
export { CspPlugin };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Module for enabling a hash-based strict Content Security Policy. */
|
|
2
|
+
declare class CSP {
|
|
3
|
+
private static readonly HASH_FUNCTION;
|
|
4
|
+
private static readonly INLINE_SCRIPT_SELECTOR;
|
|
5
|
+
private static readonly SOURCED_SCRIPT_SELECTOR;
|
|
6
|
+
private $;
|
|
7
|
+
constructor(html: string);
|
|
8
|
+
serializeDom(): string;
|
|
9
|
+
/**
|
|
10
|
+
* Returns a strict Content Security Policy for mittigating XSS.
|
|
11
|
+
* For more details read csp.withgoogle.com.
|
|
12
|
+
* If you modify this CSP, make sure it has not become trivially bypassable by
|
|
13
|
+
* checking the policy using csp-evaluator.withgoogle.com.
|
|
14
|
+
* @param hashes A list of sha-256 hashes of trusted inline scripts.
|
|
15
|
+
* @param cspOptions
|
|
16
|
+
* @param enableTrustedTypes If Trusted Types should be enabled for scripts.
|
|
17
|
+
* @param enableBrowserFallbacks If fallbacks for older browsers should be
|
|
18
|
+
* added. This is will not weaken the policy as modern browsers will ignore
|
|
19
|
+
* the fallbacks.
|
|
20
|
+
* @param enableUnsafeEval If you cannot remove all uses of eval(), you can
|
|
21
|
+
* still set a strict CSP, but you will have to use the 'unsafe-eval'
|
|
22
|
+
* keyword which will make your policy slightly less secure.
|
|
23
|
+
* @param cspOptions.enableBrowserFallbacks
|
|
24
|
+
* @param cspOptions.enableTrustedTypes
|
|
25
|
+
* @param cspOptions.enableUnsafeEval
|
|
26
|
+
* @returns A strict Content Security Policy string.
|
|
27
|
+
*/
|
|
28
|
+
static getStrictCsp(hashes?: string[], cspOptions?: {
|
|
29
|
+
enableUnsafeEval?: boolean;
|
|
30
|
+
}): string;
|
|
31
|
+
/**
|
|
32
|
+
* Enables a CSP via a meta tag at the beginning of the document.
|
|
33
|
+
* Warning: It's recommended to set CSP as HTTP response header instead of
|
|
34
|
+
* using a meta tag. Injections before the meta tag will not be covered by CSP
|
|
35
|
+
* and meta tags don't support CSP in report-only mode.
|
|
36
|
+
* @param csp A Content Security Policy string.
|
|
37
|
+
*/
|
|
38
|
+
addMetaTag(csp: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Replaces all sourced scripts with a single inline script that can be hashed
|
|
41
|
+
*/
|
|
42
|
+
refactorSourcedScriptsForHashBasedCsp(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Returns a list of hashes of all inline scripts found in the HTML document.
|
|
45
|
+
* @returns A list of sha-256 hashes of inline scripts.
|
|
46
|
+
*/
|
|
47
|
+
hashAllInlineScripts(): string[];
|
|
48
|
+
/**
|
|
49
|
+
* Returns JS code for dynamically loading sourced (external) scripts.
|
|
50
|
+
* @param scriptInfoList A list of objects containing src and type for scripts that should be loaded
|
|
51
|
+
* @returns JS code for loading scripts.
|
|
52
|
+
*/
|
|
53
|
+
static createLoaderScript(scriptInfoList: {
|
|
54
|
+
src: string;
|
|
55
|
+
type?: string;
|
|
56
|
+
}[]): string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Calculates a CSP compatible hash of an inline script.
|
|
59
|
+
* @param scriptText Text between opening and closing script tag. Has to
|
|
60
|
+
* include whitespaces and newlines!
|
|
61
|
+
* @returns A sha-256 hash of the script.
|
|
62
|
+
*/
|
|
63
|
+
static hashInlineScript(scriptText: string): string;
|
|
64
|
+
}
|
|
65
|
+
export { CSP };
|