|
1 | | -import { HTMLRewriter } from "./html_rewriter_wrapper.ts"; |
2 | | -import { Element } from "./types.d.ts"; |
3 | | - |
4 | 1 | import { default as init } from "../pkg/csp_nonce_html_transformer.js"; |
5 | 2 | await init(); |
6 | | -type Params = { |
7 | | - /** |
8 | | - * When true, uses the `Content-Security-Policy-Report-Only` header instead |
9 | | - * of the `Content-Security-Policy` header. Setting to `true` is useful for |
10 | | - * testing the CSP with real production traffic without actually blocking resources. |
11 | | - */ |
12 | | - reportOnly?: boolean; |
13 | | - /** |
14 | | - * The relative or absolute URL to report any violations. If left undefined, |
15 | | - * violations will not be reported anywhere, which this plugin deploys. If |
16 | | - * the response already has a `report-uri` directive defined in its CSP header, |
17 | | - * then that value will take precedence. |
18 | | - */ |
19 | | - reportUri?: string; |
20 | | - /** |
21 | | - * When true, adds `'unsafe-eval'` to the CSP for easier adoption. Set to |
22 | | - * `false` to have a safer policy if your code and code dependencies does |
23 | | - * not use `eval()`. |
24 | | - */ |
25 | | - unsafeEval?: boolean; |
26 | | - strictDynamic?: boolean; |
27 | | - unsafeInline?: boolean; |
28 | | - self?: boolean; |
29 | | - https?: boolean; |
30 | | - http?: boolean; |
31 | | - /** |
32 | | - * A number from 0 to 1, but 0 to 100 is also supported, along with a trailing %. |
33 | | - * |
34 | | - * You can ramp up or ramp down the inclusion of the `Content-Security-Policy` |
35 | | - * header by setting this to a value between `0` and `1`. |
36 | | - * |
37 | | - * Any value in between `0` and `1` will include the nonce in randomly distributed traffic. |
38 | | - * |
39 | | - * For example, a value of `0.25` will put the new directives in the `Content-Security-Policy` |
40 | | - * header for 25% of responses. The other 75% of responses will have the new directives |
41 | | - * in the `Content-Security-Policy-Report-Only` header. |
42 | | - */ |
43 | | - distribution?: string; |
44 | | -}; |
45 | | - |
46 | | -const hexOctets: string[] = []; |
47 | | - |
48 | | -for (let i = 0; i <= 255; ++i) { |
49 | | - const hexOctet = i.toString(16).padStart(2, "0"); |
50 | | - hexOctets.push(hexOctet); |
51 | | -} |
52 | | - |
53 | | -function uInt8ArrayToBase64String(input: Uint8Array): string { |
54 | | - let res = ""; |
55 | | - |
56 | | - for (let i = 0; i < input.length; i++) { |
57 | | - res += String.fromCharCode(parseInt(hexOctets[input[i]], 16)); |
58 | | - } |
59 | | - |
60 | | - return btoa(res); |
61 | | -} |
62 | | - |
63 | | -export function csp(originalResponse: Response, params?: Params) { |
64 | | - const isHTMLResponse = originalResponse.headers.get("content-type") |
65 | | - ?.startsWith( |
66 | | - "text/html", |
67 | | - ); |
68 | | - if (!isHTMLResponse) { |
69 | | - return originalResponse; |
70 | | - } |
71 | | - const response = new Response(originalResponse.body, originalResponse); |
72 | | - |
73 | | - let header = params && params.reportOnly |
74 | | - ? "content-security-policy-report-only" |
75 | | - : "content-security-policy"; |
76 | | - |
77 | | - // distribution is a number from 0 to 1, |
78 | | - // but 0 to 100 is also supported, along with a trailing % |
79 | | - const distribution = params?.distribution; |
80 | | - if (distribution) { |
81 | | - const threshold = distribution.endsWith("%") || parseFloat(distribution) > 1 |
82 | | - ? Math.max(parseFloat(distribution) / 100, 0) |
83 | | - : Math.max(parseFloat(distribution), 0); |
84 | | - const random = Math.random(); |
85 | | - // if a roll of the dice is greater than our threshold... |
86 | | - if (random > threshold && threshold <= 1) { |
87 | | - if (header === "content-security-policy") { |
88 | | - // if the real CSP is set, then change to report only |
89 | | - header = "content-security-policy-report-only"; |
90 | | - } else { |
91 | | - // if the CSP is set to report-only, return unadulterated response |
92 | | - return response; |
93 | | - } |
94 | | - } |
95 | | - } |
96 | | - |
97 | | - const nonce = uInt8ArrayToBase64String( |
98 | | - crypto.getRandomValues(new Uint8Array(24)), |
99 | | - ); |
100 | | - |
101 | | - const rules = [ |
102 | | - `'nonce-${nonce}'`, |
103 | | - params?.unsafeEval && `'unsafe-eval'`, |
104 | | - params?.strictDynamic && `'strict-dynamic'`, |
105 | | - params?.unsafeInline && `'unsafe-inline'`, |
106 | | - params?.self && `'self'`, |
107 | | - params?.https && `https:`, |
108 | | - params?.http && `http:`, |
109 | | - ].filter(Boolean); |
110 | | - const scriptSrc = `script-src ${rules.join(" ")}`; |
111 | | - |
112 | | - const csp = response.headers.get(header) as string; |
113 | | - if (csp) { |
114 | | - const directives = csp |
115 | | - .split(";") |
116 | | - .map((directive) => { |
117 | | - // prepend our rules for any existing directives |
118 | | - const d = directive.trim(); |
119 | | - // intentionally add trailing space to avoid mangling `script-src-elem` |
120 | | - if (d.startsWith("script-src ")) { |
121 | | - // append with trailing space to include any user-supplied values |
122 | | - // https://github.com/netlify/plugin-csp-nonce/issues/72 |
123 | | - return d.replace("script-src ", `${scriptSrc} `).trim(); |
124 | | - } |
125 | | - // intentionally omit report-uri: theirs should take precedence |
126 | | - return d; |
127 | | - }) |
128 | | - .filter(Boolean); |
129 | | - // push our rules if the directives don't exist yet |
130 | | - if (!directives.find((d) => d.startsWith("script-src "))) { |
131 | | - directives.push(scriptSrc); |
132 | | - } |
133 | | - if ( |
134 | | - params?.reportUri && !directives.find((d) => d.startsWith("report-uri")) |
135 | | - ) { |
136 | | - directives.push(`report-uri ${params.reportUri}`); |
137 | | - } |
138 | | - const value = directives.join("; "); |
139 | | - response.headers.set(header, value); |
140 | | - } else { |
141 | | - // make a new ruleset of directives if no CSP present |
142 | | - const value = [scriptSrc]; |
143 | | - if (params?.reportUri) { |
144 | | - value.push(`report-uri ${params.reportUri}`); |
145 | | - } |
146 | | - response.headers.set(header, value.join("; ")); |
147 | | - } |
148 | 3 |
|
149 | | - const querySelectors = ["script", 'link[rel="preload"][as="script"]']; |
150 | | - return new HTMLRewriter() |
151 | | - .on(querySelectors.join(","), { |
152 | | - element(element: Element) { |
153 | | - element.setAttribute("nonce", nonce); |
154 | | - }, |
155 | | - }) |
156 | | - .transform(response); |
157 | | -} |
| 4 | +export { csp } from "./csp.ts"; |
0 commit comments