@netlify/plugin-csp-nonce 1.1.4 → 1.2.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/README.md +9 -4
- package/index.js +17 -0
- package/package.json +1 -1
- package/src/__csp-nonce.ts +18 -8
package/README.md
CHANGED
|
@@ -90,12 +90,17 @@ Also, monitor the edge function logs in the Netlify UI. If the edge function is
|
|
|
90
90
|
If your HTML does not contain the `nonce` attribute on the `<script>` tags that you expect, ensure that all of these criteria are met:
|
|
91
91
|
|
|
92
92
|
- The request method is `GET`
|
|
93
|
-
- The `accept` request header starts with `text/html`, or the `user-agent` request header starts with `curl/`
|
|
94
93
|
- The `content-type` response header starts with `text/html`
|
|
95
94
|
- The path of the request is satisfied by the `path` config option, and not included in the `excludedPath` config option
|
|
96
95
|
|
|
97
|
-
###
|
|
96
|
+
### Controlling rollout
|
|
98
97
|
|
|
99
|
-
You may want to
|
|
98
|
+
You may want to gradually rollout the effects of this plugin while you monitor violation reports, without modifying code.
|
|
100
99
|
|
|
101
|
-
|
|
100
|
+
You can ramp up or ramp down the inclusion of the headers this plugin enforces by setting the `CSP_NONCE_DISTRIBUTION` environment variable to a value between `0` and `1`.
|
|
101
|
+
|
|
102
|
+
- If `0`, the plugin is completely skipped at build time, and no extra functions or edge functions get deployed. Functionally, this acts the same as if the plugin isn't installed at all.
|
|
103
|
+
- If `1`, 100% of traffic for all matching paths will include the nonce. Functionally, this acts the same as if the `CSP_NONCE_DISTRIBUTION` environment variable was not defined.
|
|
104
|
+
- Any value in between `0` and `1` will include the nonce in randomly distributed traffic. For example, a value of `0.25` will include the nonce 25% of the time for matching paths.
|
|
105
|
+
|
|
106
|
+
The `CSP_NONCE_DISTRIBUTION` environment variable needs to be scoped to both `Builds` and `Functions`.
|
package/index.js
CHANGED
|
@@ -7,11 +7,28 @@ export const onPreBuild = async ({ inputs, netlifyConfig, utils }) => {
|
|
|
7
7
|
const config = JSON.stringify(inputs, null, 2);
|
|
8
8
|
const { build } = netlifyConfig;
|
|
9
9
|
|
|
10
|
+
// DISABLE_CSP_NONCE is undocumented (deprecated), but still supported
|
|
11
|
+
// -> superseded by CSP_NONCE_DISTRIBUTION
|
|
10
12
|
if (build.environment.DISABLE_CSP_NONCE === "true") {
|
|
11
13
|
console.log(` DISABLE_CSP_NONCE environment variable is true, skipping.`);
|
|
12
14
|
return;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
// CSP_NONCE_DISTRIBUTION is a number from 0 to 1,
|
|
18
|
+
// but 0 to 100 is also supported, along with a trailing %
|
|
19
|
+
const distribution = build.environment.CSP_NONCE_DISTRIBUTION;
|
|
20
|
+
if (!!distribution) {
|
|
21
|
+
const threshold =
|
|
22
|
+
distribution.endsWith("%") || parseFloat(distribution) > 1
|
|
23
|
+
? Math.max(parseFloat(distribution) / 100, 0)
|
|
24
|
+
: Math.max(parseFloat(distribution), 0);
|
|
25
|
+
console.log(` CSP_NONCE_DISTRIBUTION is set to ${threshold * 100}%`);
|
|
26
|
+
if (threshold === 0) {
|
|
27
|
+
console.log(` Skipping.`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
console.log(` Current working directory: ${process.cwd()}`);
|
|
16
33
|
const basePath =
|
|
17
34
|
build.environment.SITE_ID === SITE_ID
|
package/package.json
CHANGED
package/src/__csp-nonce.ts
CHANGED
|
@@ -25,26 +25,36 @@ const handler = async (request: Request, context: Context) => {
|
|
|
25
25
|
// for debugging which routes use this edge function
|
|
26
26
|
response.headers.set("x-debug-csp-nonce", "invoked");
|
|
27
27
|
|
|
28
|
-
// html
|
|
28
|
+
// html GETs only
|
|
29
29
|
const isGET = request.method?.toUpperCase() === "GET";
|
|
30
|
-
const isCurl = request.headers.get("user-agent")?.startsWith("curl/");
|
|
31
|
-
const isHTMLRequest =
|
|
32
|
-
request.headers.get("accept")?.includes("text/html") ||
|
|
33
|
-
request.headers.get("sec-fetch-dest") === "document" ||
|
|
34
|
-
isCurl;
|
|
35
30
|
const isHTMLResponse = response.headers
|
|
36
31
|
.get("content-type")
|
|
37
32
|
?.startsWith("text/html");
|
|
38
|
-
const shouldTransformResponse = isGET &&
|
|
33
|
+
const shouldTransformResponse = isGET && isHTMLResponse;
|
|
39
34
|
if (!shouldTransformResponse) {
|
|
40
35
|
console.log(`Unnecessary invocation for ${request.url}`, {
|
|
41
36
|
method: request.method,
|
|
42
|
-
accept: request.headers.get("accept"),
|
|
43
37
|
"content-type": response.headers.get("content-type"),
|
|
44
38
|
});
|
|
45
39
|
return response;
|
|
46
40
|
}
|
|
47
41
|
|
|
42
|
+
// CSP_NONCE_DISTRIBUTION is a number from 0 to 1,
|
|
43
|
+
// but 0 to 100 is also supported, along with a trailing %
|
|
44
|
+
// @ts-expect-error
|
|
45
|
+
const distribution = Netlify.env.get("CSP_NONCE_DISTRIBUTION");
|
|
46
|
+
if (!!distribution) {
|
|
47
|
+
const threshold =
|
|
48
|
+
distribution.endsWith("%") || parseFloat(distribution) > 1
|
|
49
|
+
? Math.max(parseFloat(distribution) / 100, 0)
|
|
50
|
+
: Math.max(parseFloat(distribution), 0);
|
|
51
|
+
// if a roll of the dice is greater than our threshold, skip
|
|
52
|
+
const random = Math.random();
|
|
53
|
+
if (random > threshold && threshold <= 1) {
|
|
54
|
+
return response;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
const nonce = randomBytes(24).toString("base64");
|
|
49
59
|
// `'strict-dynamic'` allows scripts to be loaded from trusted scripts
|
|
50
60
|
// when `'strict-dynamic'` is present, `'unsafe-inline' 'self' https: http:` is ignored by browsers
|