@netlify/plugin-csp-nonce 1.1.4 → 1.2.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/README.md +9 -4
- package/index.js +17 -0
- package/package.json +1 -1
- package/src/__csp-nonce.ts +28 -12
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 `Content-Security-Policy` header 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 put the nonce in the `Content-Security-Policy` header 25% of requests for matching paths. The other 75% of matching requests will have the nonce in the `Content-Security-Policy-Report-Only` header.
|
|
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
|
@@ -15,36 +15,52 @@ type Params = {
|
|
|
15
15
|
};
|
|
16
16
|
const params = inputs as Params;
|
|
17
17
|
|
|
18
|
-
const header = params.reportOnly
|
|
19
|
-
? "content-security-policy-report-only"
|
|
20
|
-
: "content-security-policy";
|
|
21
|
-
|
|
22
18
|
const handler = async (request: Request, context: Context) => {
|
|
23
19
|
const response = await context.next();
|
|
24
20
|
|
|
21
|
+
let header = params.reportOnly
|
|
22
|
+
? "content-security-policy-report-only"
|
|
23
|
+
: "content-security-policy";
|
|
24
|
+
|
|
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
|
+
const random = Math.random();
|
|
52
|
+
// if a roll of the dice is greater than our threshold...
|
|
53
|
+
if (random > threshold && threshold <= 1) {
|
|
54
|
+
if (header === "content-security-policy") {
|
|
55
|
+
// if the real CSP is set, then change to report only
|
|
56
|
+
header = "content-security-policy-report-only";
|
|
57
|
+
} else {
|
|
58
|
+
// if the CSP is set to report-only, return unadulterated response
|
|
59
|
+
return response;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
const nonce = randomBytes(24).toString("base64");
|
|
49
65
|
// `'strict-dynamic'` allows scripts to be loaded from trusted scripts
|
|
50
66
|
// when `'strict-dynamic'` is present, `'unsafe-inline' 'self' https: http:` is ignored by browsers
|