@netlify/plugin-csp-nonce 1.1.3 → 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 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
- ### Quickly enabling and disabling
96
+ ### Controlling rollout
98
97
 
99
- You may want to quickly enable/disable the plugin while monitoring violation reports. You can do so without modifying code.
98
+ You may want to gradually rollout the effects of this plugin while you monitor violation reports, without modifying code.
100
99
 
101
- Simply set the `DISABLE_CSP_NONCE` environment variable to `true`, and your next deploy will skip running the plugin. Setting to `false` will re-enable the plugin. The environment variable needs to be scoped to `Builds`.
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@netlify/plugin-csp-nonce",
4
- "version": "1.1.3",
4
+ "version": "1.2.0",
5
5
  "description": "Use a nonce for the script-src and style-src directives of your Content Security Policy.",
6
6
  "main": "index.js",
7
7
  "repository": {
@@ -25,22 +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/curl GETs only
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 && isHTMLRequest && isHTMLResponse;
33
+ const shouldTransformResponse = isGET && isHTMLResponse;
39
34
  if (!shouldTransformResponse) {
40
- console.log(`Unnecessary invocation for ${request.url}`);
35
+ console.log(`Unnecessary invocation for ${request.url}`, {
36
+ method: request.method,
37
+ "content-type": response.headers.get("content-type"),
38
+ });
41
39
  return response;
42
40
  }
43
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
+
44
58
  const nonce = randomBytes(24).toString("base64");
45
59
  // `'strict-dynamic'` allows scripts to be loaded from trusted scripts
46
60
  // when `'strict-dynamic'` is present, `'unsafe-inline' 'self' https: http:` is ignored by browsers