@elliemae/pui-cli 8.40.3 → 8.41.1
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/csp.js +4 -20
- package/dist/cjs/server/middlewares.js +5 -0
- 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/csp.js +4 -20
- package/dist/esm/server/middlewares.js +5 -0
- 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
package/dist/cjs/server/csp.js
CHANGED
|
@@ -39,7 +39,6 @@ var import_express = __toESM(require("express"), 1);
|
|
|
39
39
|
var import_helmet = __toESM(require("helmet"), 1);
|
|
40
40
|
const CSP_REPORT_URI = "/diagnostics/v1/csp";
|
|
41
41
|
const sources = [
|
|
42
|
-
"'self'",
|
|
43
42
|
"http://localhost:*",
|
|
44
43
|
"https://localhost:*",
|
|
45
44
|
"ws://localhost:*",
|
|
@@ -47,12 +46,7 @@ const sources = [
|
|
|
47
46
|
"*.elliemae.io",
|
|
48
47
|
"*.elliemae.com",
|
|
49
48
|
"*.ellieservices.com",
|
|
50
|
-
"*.ellielabs.com"
|
|
51
|
-
"https://cdn.appdynamics.com",
|
|
52
|
-
"http://pdx-col.eum-appdynamics.com",
|
|
53
|
-
"https://pdx-col.eum-appdynamics.com/",
|
|
54
|
-
"https://www.google-analytics.com",
|
|
55
|
-
"https://www.googletagmanager.com"
|
|
49
|
+
"*.ellielabs.com"
|
|
56
50
|
];
|
|
57
51
|
const sendFileWithCSPNonce = ({
|
|
58
52
|
buildPath,
|
|
@@ -72,7 +66,7 @@ const sendFileWithCSPNonce = ({
|
|
|
72
66
|
};
|
|
73
67
|
const getScriptSrc = () => {
|
|
74
68
|
const source = (req, res) => `'nonce-${res.locals.cspNonce}'`;
|
|
75
|
-
const scriptSrc =
|
|
69
|
+
const scriptSrc = [source, "strict-dynamic"];
|
|
76
70
|
return true ? scriptSrc.concat(["'unsafe-eval'"]) : scriptSrc;
|
|
77
71
|
};
|
|
78
72
|
const csp = (app) => {
|
|
@@ -84,24 +78,14 @@ const csp = (app) => {
|
|
|
84
78
|
(0, import_helmet.default)({
|
|
85
79
|
contentSecurityPolicy: {
|
|
86
80
|
directives: {
|
|
87
|
-
baseUri: ["'
|
|
88
|
-
connectSrc: sources,
|
|
89
|
-
defaultSrc: ["'self'"],
|
|
90
|
-
fontSrc: sources.concat(["data:"]),
|
|
91
|
-
formAction: ["'self'"],
|
|
81
|
+
baseUri: ["'none'"],
|
|
92
82
|
frameAncestors: sources,
|
|
93
|
-
frameSrc: sources,
|
|
94
|
-
imgSrc: sources.concat(["data:"]),
|
|
95
83
|
objectSrc: ["'none'"],
|
|
96
84
|
scriptSrc: getScriptSrc(),
|
|
97
|
-
scriptSrcAttr: ["'none'"],
|
|
98
|
-
styleSrc: sources.concat(["'unsafe-inline'"]),
|
|
99
|
-
workerSrc: sources,
|
|
100
85
|
upgradeInsecureRequests: [],
|
|
101
|
-
reportUri: CSP_REPORT_URI,
|
|
102
86
|
reportTo: CSP_REPORT_URI
|
|
103
87
|
},
|
|
104
|
-
reportOnly:
|
|
88
|
+
reportOnly: process.env.CSP_REPORT_ONLY !== "false"
|
|
105
89
|
},
|
|
106
90
|
xFrameOptions: false,
|
|
107
91
|
xPermittedCrossDomainPolicies: false,
|
|
@@ -54,6 +54,11 @@ const setupDefaultMiddlewares = (app) => {
|
|
|
54
54
|
app.use((0, import_cors.default)());
|
|
55
55
|
app.options("*", (0, import_cors.default)());
|
|
56
56
|
(0, import_csp.csp)(app);
|
|
57
|
+
app.set("Cross-Origin-Opener-Policy", "same-origin-allow-popups");
|
|
58
|
+
app.set(
|
|
59
|
+
"Permissions-Policy",
|
|
60
|
+
"geolocation=(), camera=(), microphone=(), interest-cohort=()"
|
|
61
|
+
);
|
|
57
62
|
app.use(import_express.default.urlencoded({ extended: false }));
|
|
58
63
|
app.use(import_express.default.text({ type: "text/plain" }));
|
|
59
64
|
app.use(import_express.default.json({ type: "application/json" }));
|
|
@@ -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;
|
package/dist/esm/server/csp.js
CHANGED
|
@@ -5,7 +5,6 @@ import express from "express";
|
|
|
5
5
|
import helmet from "helmet";
|
|
6
6
|
const CSP_REPORT_URI = "/diagnostics/v1/csp";
|
|
7
7
|
const sources = [
|
|
8
|
-
"'self'",
|
|
9
8
|
"http://localhost:*",
|
|
10
9
|
"https://localhost:*",
|
|
11
10
|
"ws://localhost:*",
|
|
@@ -13,12 +12,7 @@ const sources = [
|
|
|
13
12
|
"*.elliemae.io",
|
|
14
13
|
"*.elliemae.com",
|
|
15
14
|
"*.ellieservices.com",
|
|
16
|
-
"*.ellielabs.com"
|
|
17
|
-
"https://cdn.appdynamics.com",
|
|
18
|
-
"http://pdx-col.eum-appdynamics.com",
|
|
19
|
-
"https://pdx-col.eum-appdynamics.com/",
|
|
20
|
-
"https://www.google-analytics.com",
|
|
21
|
-
"https://www.googletagmanager.com"
|
|
15
|
+
"*.ellielabs.com"
|
|
22
16
|
];
|
|
23
17
|
const sendFileWithCSPNonce = ({
|
|
24
18
|
buildPath,
|
|
@@ -38,7 +32,7 @@ const sendFileWithCSPNonce = ({
|
|
|
38
32
|
};
|
|
39
33
|
const getScriptSrc = () => {
|
|
40
34
|
const source = (req, res) => `'nonce-${res.locals.cspNonce}'`;
|
|
41
|
-
const scriptSrc =
|
|
35
|
+
const scriptSrc = [source, "strict-dynamic"];
|
|
42
36
|
return true ? scriptSrc.concat(["'unsafe-eval'"]) : scriptSrc;
|
|
43
37
|
};
|
|
44
38
|
const csp = (app) => {
|
|
@@ -50,24 +44,14 @@ const csp = (app) => {
|
|
|
50
44
|
helmet({
|
|
51
45
|
contentSecurityPolicy: {
|
|
52
46
|
directives: {
|
|
53
|
-
baseUri: ["'
|
|
54
|
-
connectSrc: sources,
|
|
55
|
-
defaultSrc: ["'self'"],
|
|
56
|
-
fontSrc: sources.concat(["data:"]),
|
|
57
|
-
formAction: ["'self'"],
|
|
47
|
+
baseUri: ["'none'"],
|
|
58
48
|
frameAncestors: sources,
|
|
59
|
-
frameSrc: sources,
|
|
60
|
-
imgSrc: sources.concat(["data:"]),
|
|
61
49
|
objectSrc: ["'none'"],
|
|
62
50
|
scriptSrc: getScriptSrc(),
|
|
63
|
-
scriptSrcAttr: ["'none'"],
|
|
64
|
-
styleSrc: sources.concat(["'unsafe-inline'"]),
|
|
65
|
-
workerSrc: sources,
|
|
66
51
|
upgradeInsecureRequests: [],
|
|
67
|
-
reportUri: CSP_REPORT_URI,
|
|
68
52
|
reportTo: CSP_REPORT_URI
|
|
69
53
|
},
|
|
70
|
-
reportOnly:
|
|
54
|
+
reportOnly: process.env.CSP_REPORT_ONLY !== "false"
|
|
71
55
|
},
|
|
72
56
|
xFrameOptions: false,
|
|
73
57
|
xPermittedCrossDomainPolicies: false,
|
|
@@ -20,6 +20,11 @@ const setupDefaultMiddlewares = (app) => {
|
|
|
20
20
|
app.use(cors());
|
|
21
21
|
app.options("*", cors());
|
|
22
22
|
csp(app);
|
|
23
|
+
app.set("Cross-Origin-Opener-Policy", "same-origin-allow-popups");
|
|
24
|
+
app.set(
|
|
25
|
+
"Permissions-Policy",
|
|
26
|
+
"geolocation=(), camera=(), microphone=(), interest-cohort=()"
|
|
27
|
+
);
|
|
23
28
|
app.use(express.urlencoded({ extended: false }));
|
|
24
29
|
app.use(express.text({ type: "text/plain" }));
|
|
25
30
|
app.use(express.json({ type: "application/json" }));
|
|
@@ -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 };
|