Skip to content

Commit 453d467

Browse files
Add CSP (#8078)
* add CSP * fix websocket edge case * update * add plausible connect * use vite to conditionally change CSP * fix vercel config * fix * update CSP * fix vercel csps * cleanup env * cleanup vars and only enable in production builds * swap csps * update vercel json * update vercel json * update vercel json * fix vercel detection * also report for preview * fix vercel detection --------- Co-authored-by: Jace Browning <[email protected]>
1 parent bddebd0 commit 453d467

File tree

6 files changed

+143
-4
lines changed

6 files changed

+143
-4
lines changed

index.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8" />
5-
<!-- PERPETUAL TODO reconsider this option.
6-
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
5+
<!--
6+
This all-blocking CSP is replaced by the vite html-transform plugin.
7+
8+
We have to use the <meta> tag because the Electron app is hosted over file:// and does not support HTTP headers.
9+
Reporting functionality is not supported with <meta> tags so we have to get this right.
710
-->
11+
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
812

913
<link rel="icon" href="/favicon.ico" />
1014
<meta name="viewport" content="width=device-width, initial-scale=1" />

src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ app.on('window-all-closed', () => {
293293
app.on('ready', (event, data) => {
294294
// Avoid potentially 2 ready fires
295295
if (mainWindow) return
296+
296297
// Create the mainWindow
297298
mainWindow = createWindow()
298299
// Set menu application to null to avoid default electron menu

vercel.json

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,26 @@
22
"version": 2,
33
"headers": [
44
{
5-
"source": "/",
5+
"source": "/(.*)",
6+
"has": [
7+
{
8+
"type": "host",
9+
"value": "app.zoo.dev"
10+
}
11+
],
12+
"headers": [
13+
{
14+
"key": "Reporting-Endpoints",
15+
"value": "csp-reporting-endpoint=\"https://csp-logger.vercel.app/csp-report\""
16+
},
17+
{
18+
"key": "Content-Security-Policy-Report-Only",
19+
"value": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * blob: 'unsafe-inline'; connect-src 'self' https://plausible.corp.zoo.dev https://api.zoo.dev wss://api.zoo.dev https://api.dev.zoo.dev wss://api.dev.zoo.dev https://api.zoogov.dev wss://api.zoogov.dev; object-src 'none'; frame-ancestors 'none'; script-src 'self' 'wasm-unsafe-eval' https://plausible.corp.zoo.dev/js/script.tagged-events.js; report-uri https://csp-logger.vercel.app/csp-report; report-to csp-reporting-endpoint;"
20+
}
21+
]
22+
},
23+
{
24+
"source": "/(.*)",
625
"missing": [
726
{
827
"type": "host",
@@ -13,6 +32,14 @@
1332
{
1433
"key": "X-Robots-Tag",
1534
"value": "noindex"
35+
},
36+
{
37+
"key": "Reporting-Endpoints",
38+
"value": "csp-reporting-endpoint=\"https://csp-logger.vercel.app/csp-report\""
39+
},
40+
{
41+
"key": "Content-Security-Policy-Report-Only",
42+
"value": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * blob: 'unsafe-inline'; connect-src 'self' https://plausible.corp.zoo.dev https://api.zoo.dev wss://api.zoo.dev https://api.dev.zoo.dev wss://api.dev.zoo.dev https://api.zoogov.dev wss://api.zoogov.dev; object-src 'none'; frame-ancestors 'none'; frame-src 'self' https://vercel.live; script-src 'self' 'wasm-unsafe-eval' https://plausible.corp.zoo.dev/js/script.tagged-events.js https://vercel.live/_next-live/feedback/feedback.js 'unsafe-eval'; report-uri https://csp-logger.vercel.app/csp-report; report-to csp-reporting-endpoint;"
1643
}
1744
]
1845
}

vite.base.config.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,107 @@ export function pluginHotRestart(command: 'reload' | 'restart'): Plugin {
115115
},
116116
}
117117
}
118+
119+
export function indexHtmlCsp(enabled: boolean): Plugin {
120+
const csp = [
121+
// By default, only allow same origin.
122+
"default-src 'self'",
123+
// Allow inline styles and styles from the same origin. This is how we use CSS rightnow.
124+
"style-src 'self' 'unsafe-inline'",
125+
// Allow images from any source and inline images. We fetch user profile images from any origin.
126+
"img-src * blob: 'unsafe-inline'",
127+
// Allow WebSocket connections and fetches to our API.
128+
"connect-src 'self' https://plausible.corp.zoo.dev https://api.zoo.dev wss://api.zoo.dev https://api.dev.zoo.dev wss://api.dev.zoo.dev https://api.zoogov.dev wss://api.zoogov.dev",
129+
// Disallow legacy stuff
130+
"object-src 'none'",
131+
]
132+
133+
// Allow scripts from the same origin and from Plausible Analytics. Allow WASM execution.
134+
const cspScriptBase =
135+
"script-src 'self' 'wasm-unsafe-eval' https://plausible.corp.zoo.dev/js/script.tagged-events.js"
136+
137+
// frame ancestors can only be blocked using HTTP headers (see vercel.json)
138+
const vercelCspBase = ["frame-ancestors 'none'"]
139+
140+
const cspReporting = [
141+
'report-uri https://csp-logger.vercel.app/csp-report',
142+
'report-to csp-reporting-endpoint',
143+
]
144+
145+
const vercelCsp =
146+
csp
147+
.concat(vercelCspBase)
148+
.concat([cspScriptBase])
149+
.concat(cspReporting)
150+
.join('; ') + ';'
151+
152+
const reportingEndpoints = {
153+
key: 'Reporting-Endpoints',
154+
value: 'csp-reporting-endpoint="https://csp-logger.vercel.app/csp-report"',
155+
}
156+
157+
console.log(
158+
'Content-Security-Policy for Vercel (prod) (vercel.json):',
159+
vercelCsp
160+
)
161+
162+
console.log(
163+
JSON.stringify(
164+
[
165+
reportingEndpoints,
166+
{
167+
key: 'Content-Security-Policy-Report-Only',
168+
value: vercelCsp,
169+
},
170+
],
171+
null,
172+
2
173+
)
174+
)
175+
176+
console.log('Content-Security-Policy for Vercel (preview) (vercel.json):')
177+
178+
console.log(
179+
JSON.stringify(
180+
[
181+
reportingEndpoints,
182+
{
183+
key: 'Content-Security-Policy-Report-Only',
184+
value:
185+
csp
186+
.concat(vercelCspBase)
187+
.concat([
188+
// vercel.live is used for feedback scripts in preview deployments.
189+
"frame-src 'self' https://vercel.live",
190+
`${cspScriptBase} https://vercel.live/_next-live/feedback/feedback.js 'unsafe-eval'`,
191+
])
192+
.concat(cspReporting)
193+
.join('; ') + ';',
194+
},
195+
],
196+
197+
null,
198+
2
199+
)
200+
)
201+
202+
return {
203+
name: 'html-transform',
204+
transformIndexHtml(html: string) {
205+
let indexHtmlRegex =
206+
/<meta\shttp-equiv="Content-Security-Policy"\scontent="(.*?)">/
207+
if (!enabled) {
208+
// Web deployments that are deployed to vercel don't need a CSP in the indexHTML.
209+
// They get it through vercel.json.
210+
return html.replace(indexHtmlRegex, '')
211+
} else {
212+
return html.replace(
213+
indexHtmlRegex,
214+
`<meta http-equiv="Content-Security-Policy" content="${
215+
csp.concat([cspScriptBase]).join('; ') + ';'
216+
}">`
217+
)
218+
}
219+
},
220+
}
221+
}

vite.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import version from 'vite-plugin-package-version'
77
import topLevelAwait from 'vite-plugin-top-level-await'
88
import viteTsconfigPaths from 'vite-tsconfig-paths'
99
import { configDefaults, defineConfig } from 'vitest/config'
10+
import { indexHtmlCsp } from './vite.base.config'
1011

1112
export default defineConfig(({ command, mode }) => {
1213
const runMillion = process.env.RUN_MILLION
@@ -77,6 +78,7 @@ export default defineConfig(({ command, mode }) => {
7778
},
7879
plugins: [
7980
react(),
81+
indexHtmlCsp(!process.env.VERCEL && mode === 'production'),
8082
viteTsconfigPaths(),
8183
eslint(),
8284
version(),

vite.renderer.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { defineConfig } from 'vite'
55
import topLevelAwait from 'vite-plugin-top-level-await'
66
import viteTsconfigPaths from 'vite-tsconfig-paths'
77

8-
import { pluginExposeRenderer } from './vite.base.config'
8+
import { pluginExposeRenderer, indexHtmlCsp } from './vite.base.config'
99

1010
// https://vitejs.dev/config
1111
export default defineConfig((env) => {
@@ -23,6 +23,7 @@ export default defineConfig((env) => {
2323
// Needed for electron-forge (in npm run tron:start)
2424
optimizeDeps: { esbuildOptions: { target: 'es2022' } },
2525
plugins: [
26+
indexHtmlCsp(true),
2627
pluginExposeRenderer(name),
2728
viteTsconfigPaths(),
2829
lezer(),

0 commit comments

Comments
 (0)