@netlify/plugin-csp-nonce 1.2.13 → 1.3.1-alpha.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.
Files changed (3) hide show
  1. package/index.js +34 -14
  2. package/package.json +14 -3
  3. package/src/__csp-nonce.ts +19 -101
package/index.js CHANGED
@@ -1,7 +1,19 @@
1
1
  /* eslint-disable no-console */
2
- import fs, { copyFileSync } from "fs";
2
+ import fs, { copyFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from 'node:url'
5
+ import { getBuildInfo } from "@netlify/build-info/node";
3
6
 
4
- const SITE_ID = "321a7119-6008-49a8-9d2f-e20602b1b349";
7
+ const __dirname = dirname(fileURLToPath(import.meta.url))
8
+
9
+ async function projectUsesNextJS() {
10
+ for (const framework of (await getBuildInfo()).frameworks) {
11
+ if (framework.id === "next") {
12
+ return true;
13
+ }
14
+ }
15
+ return false;
16
+ }
5
17
 
6
18
  export const onPreBuild = async ({
7
19
  inputs,
@@ -9,7 +21,6 @@ export const onPreBuild = async ({
9
21
  utils,
10
22
  constants,
11
23
  }) => {
12
- const config = JSON.stringify(inputs, null, 2);
13
24
  const { build } = netlifyConfig;
14
25
 
15
26
  const { INTERNAL_EDGE_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC } = constants;
@@ -37,23 +48,30 @@ export const onPreBuild = async ({
37
48
  }
38
49
 
39
50
  console.log(` Current working directory: ${process.cwd()}`);
40
- const basePath =
41
- build.environment.SITE_ID === SITE_ID
42
- ? "./src"
43
- : "./node_modules/@netlify/plugin-csp-nonce/src";
44
51
 
45
52
  // make the directory in case it actually doesn't exist yet
46
53
  await utils.run.command(`mkdir -p ${INTERNAL_EDGE_FUNCTIONS_SRC}`);
47
54
  console.log(
48
- ` Writing nonce edge function to ${INTERNAL_EDGE_FUNCTIONS_SRC}...`,
55
+ ` Writing nonce edge function to ${INTERNAL_EDGE_FUNCTIONS_SRC}...`
49
56
  );
50
57
  copyFileSync(
51
- `${basePath}/__csp-nonce.ts`,
52
- `${INTERNAL_EDGE_FUNCTIONS_SRC}/__csp-nonce.ts`,
58
+ resolve(__dirname, `./src/__csp-nonce.ts`),
59
+ `${INTERNAL_EDGE_FUNCTIONS_SRC}/__csp-nonce.ts`
53
60
  );
61
+
62
+ const usesNext = await projectUsesNextJS();
63
+
64
+ // Do not invoke the CSP Edge Function for Netlify Image CDN requests.
65
+ inputs.excludedPath.push("/.netlify/images");
66
+
67
+ // If using NextJS, do not invoke the CSP Edge Function for NextJS Image requests.
68
+ if (usesNext) {
69
+ inputs.excludedPath.push("/_next/image");
70
+ }
71
+
54
72
  fs.writeFileSync(
55
73
  `${INTERNAL_EDGE_FUNCTIONS_SRC}/__csp-nonce-inputs.json`,
56
- config,
74
+ JSON.stringify(inputs, null, 2)
57
75
  );
58
76
 
59
77
  // if no reportUri in config input, deploy function on site's behalf
@@ -61,11 +79,11 @@ export const onPreBuild = async ({
61
79
  // make the directory in case it actually doesn't exist yet
62
80
  await utils.run.command(`mkdir -p ${INTERNAL_FUNCTIONS_SRC}`);
63
81
  console.log(
64
- ` Writing violations logging function to ${INTERNAL_FUNCTIONS_SRC}...`,
82
+ ` Writing violations logging function to ${INTERNAL_FUNCTIONS_SRC}...`
65
83
  );
66
84
  copyFileSync(
67
- `${basePath}/__csp-violations.ts`,
68
- `${INTERNAL_FUNCTIONS_SRC}/__csp-violations.ts`,
85
+ resolve(__dirname, `./src/__csp-violations.ts`),
86
+ `${INTERNAL_FUNCTIONS_SRC}/__csp-violations.ts`
69
87
  );
70
88
  } else {
71
89
  console.log(` Using ${inputs.reportUri} as report-uri directive...`);
@@ -73,3 +91,5 @@ export const onPreBuild = async ({
73
91
 
74
92
  console.log(` Done.`);
75
93
  };
94
+
95
+ export const onPreDev = onPreBuild;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@netlify/plugin-csp-nonce",
4
- "version": "1.2.13",
4
+ "version": "1.3.1-alpha.1",
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": {
@@ -17,14 +17,25 @@
17
17
  "src/*"
18
18
  ],
19
19
  "devDependencies": {
20
+ "@types/node": "^22.9.0",
21
+ "cheerio": "^1.0.0",
22
+ "content-security-policy-parser": "^0.6.0",
23
+ "execa": "^9.5.1",
24
+ "get-port": "^7.1.0",
20
25
  "prettier": "^2.8.8",
21
- "typescript": "^5.2.2"
26
+ "strip-ansi": "^7.1.0",
27
+ "typescript": "^5.2.2",
28
+ "vitest": "^2.1.4"
22
29
  },
23
30
  "bugs": {
24
31
  "url": "https://github.com/netlify/plugin-csp-nonce/issues"
25
32
  },
26
33
  "homepage": "https://github.com/netlify/plugin-csp-nonce#readme",
27
34
  "scripts": {
28
- "build": "tsc src/*.ts --noEmit --strict --lib es2018,dom"
35
+ "build": "tsc src/*.ts --noEmit --strict --lib es2018,dom",
36
+ "test": "vitest"
37
+ },
38
+ "dependencies": {
39
+ "@netlify/build-info": "^7.15.2"
29
40
  }
30
41
  }
@@ -2,10 +2,7 @@
2
2
  // @ts-ignore
3
3
  import type { Config, Context } from "netlify:edge";
4
4
  // @ts-ignore
5
- import { randomBytes } from "node:crypto";
6
- // @ts-ignore
7
- import { HTMLRewriter } from "https://ghuc.cc/worker-tools/html-rewriter@0.1.0-pre.19/index.ts";
8
-
5
+ import { csp } from "https://deno.land/x/csp_nonce_html_transformer@v2.1.1/src/index.ts";
9
6
  // @ts-ignore
10
7
  import inputs from "./__csp-nonce-inputs.json" assert { type: "json" };
11
8
 
@@ -15,109 +12,30 @@ type Params = {
15
12
  unsafeEval: boolean;
16
13
  path: string | string[];
17
14
  excludedPath: string[];
15
+ distribution?: string;
16
+ strictDynamic?: boolean;
17
+ unsafeInline?: boolean;
18
+ self?: boolean;
19
+ https?: boolean;
20
+ http?: boolean;
18
21
  };
19
22
  const params = inputs as Params;
23
+ params.reportUri = params.reportUri || "/.netlify/functions/__csp-violations";
24
+ // @ts-ignore
25
+ params.distribution = Netlify.env.get("CSP_NONCE_DISTRIBUTION");
26
+
27
+ params.strictDynamic = true;
28
+ params.unsafeInline = true;
29
+ params.self = true;
30
+ params.https = true;
31
+ params.http = true;
20
32
 
21
33
  const handler = async (request: Request, context: Context) => {
22
34
  const response = await context.next(request);
23
35
 
24
36
  // for debugging which routes use this edge function
25
37
  response.headers.set("x-debug-csp-nonce", "invoked");
26
-
27
- const isHTMLResponse = response.headers.get("content-type") === "text/html";
28
- if (!isHTMLResponse) {
29
- console.log(`Unnecessary invocation for ${request.url}`, {
30
- method: request.method,
31
- "content-type": response.headers.get("content-type"),
32
- });
33
- return response;
34
- }
35
-
36
- let header = params.reportOnly
37
- ? "content-security-policy-report-only"
38
- : "content-security-policy";
39
-
40
- // CSP_NONCE_DISTRIBUTION is a number from 0 to 1,
41
- // but 0 to 100 is also supported, along with a trailing %
42
- // @ts-ignore
43
- const distribution = Netlify.env.get("CSP_NONCE_DISTRIBUTION");
44
- if (!!distribution) {
45
- const threshold =
46
- distribution.endsWith("%") || parseFloat(distribution) > 1
47
- ? Math.max(parseFloat(distribution) / 100, 0)
48
- : Math.max(parseFloat(distribution), 0);
49
- const random = Math.random();
50
- // if a roll of the dice is greater than our threshold...
51
- if (random > threshold && threshold <= 1) {
52
- if (header === "content-security-policy") {
53
- // if the real CSP is set, then change to report only
54
- header = "content-security-policy-report-only";
55
- } else {
56
- // if the CSP is set to report-only, return unadulterated response
57
- return response;
58
- }
59
- }
60
- }
61
-
62
- const nonce = randomBytes(24).toString("base64");
63
- // `'strict-dynamic'` allows scripts to be loaded from trusted scripts
64
- // when `'strict-dynamic'` is present, `'unsafe-inline' 'self' https: http:` is ignored by browsers
65
- // `'unsafe-inline' 'self' https: http:` is a compat check for browsers that don't support `strict-dynamic`
66
- // https://content-security-policy.com/strict-dynamic/
67
- const rules = [
68
- `'nonce-${nonce}'`,
69
- `'strict-dynamic'`,
70
- `'unsafe-inline'`,
71
- params.unsafeEval && `'unsafe-eval'`,
72
- `'self'`,
73
- `https:`,
74
- `http:`,
75
- ].filter(Boolean);
76
- const scriptSrc = `script-src ${rules.join(" ")}`;
77
- const reportUri = `report-uri ${
78
- params.reportUri || "/.netlify/functions/__csp-violations"
79
- }`;
80
-
81
- const csp = response.headers.get(header) as string;
82
- if (csp) {
83
- const directives = csp
84
- .split(";")
85
- .map((directive) => {
86
- // prepend our rules for any existing directives
87
- const d = directive.trim();
88
- // intentionally add trailing space to avoid mangling `script-src-elem`
89
- if (d.startsWith("script-src ")) {
90
- // append with trailing space to include any user-supplied values
91
- // https://github.com/netlify/plugin-csp-nonce/issues/72
92
- return d.replace("script-src ", `${scriptSrc} `).trim();
93
- }
94
- // intentionally omit report-uri: theirs should take precedence
95
- return d;
96
- })
97
- .filter(Boolean);
98
- // push our rules if the directives don't exist yet
99
- if (!directives.find((d) => d.startsWith("script-src "))) {
100
- directives.push(scriptSrc);
101
- }
102
- if (!directives.find((d) => d.startsWith("report-uri"))) {
103
- directives.push(reportUri);
104
- }
105
- const value = directives.join("; ");
106
- response.headers.set(header, value);
107
- } else {
108
- // make a new ruleset of directives if no CSP present
109
- const value = [scriptSrc, reportUri].join("; ");
110
- response.headers.set(header, value);
111
- }
112
-
113
- const querySelectors = ["script", 'link[rel="preload"][as="script"]'];
114
- return new HTMLRewriter()
115
- .on(querySelectors.join(","), {
116
- element(element: HTMLElement) {
117
- element.setAttribute("nonce", nonce);
118
- },
119
- })
120
- .transform(response);
38
+ return csp(response, params)
121
39
  };
122
40
 
123
41
  // Top 50 most common extensions (minus .html and .htm) according to Humio
@@ -175,8 +93,8 @@ const excludedExtensions = [
175
93
  export const config: Config = {
176
94
  path: params.path,
177
95
  excludedPath: ["/.netlify*", `**/*.(${excludedExtensions.join("|")})`]
178
- .concat(params.excludedPath)
179
- .filter(Boolean),
96
+ .concat(params.excludedPath)
97
+ .filter(Boolean),
180
98
  handler,
181
99
  onError: "bypass",
182
100
  method: "GET",