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