Skip to content
Closed

Add CSP #7990

Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- PERPETUAL TODO reconsider this option.
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<!--
This all-blocking CSP is replaced by the vite html-transform plugin.

We have to use the <meta> tag because the Electron app is hosted over file:// and does not support HTTP headers.
Reporting functionality is not supported with <meta> tags so we have to get this right.
-->
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">

<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ app.on('window-all-closed', () => {
app.on('ready', (event, data) => {
// Avoid potentially 2 ready fires
if (mainWindow) return

// Create the mainWindow
mainWindow = createWindow()
// Set menu application to null to avoid default electron menu
Expand Down
15 changes: 14 additions & 1 deletion vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@
"version": 2,
"headers": [
{
"source": "/",
"source": "/(.*)",
"headers": [
{
"key": "Reporting-Endpoints",
"value": "csp-reporting-endpoint=\"https://csp-logger.vercel.app/csp-report\""
},
{
"key": "Content-Security-Policy-Report-Only",
"value": "default-src 'self';style-src 'self' 'unsafe-inline';img-src * blob: 'unsafe-inline';script-src 'self' 'wasm-unsafe-eval' https://plausible.corp.zoo.dev/js/script.tagged-events.js;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;object-src 'none';frame-ancestors 'none';report-uri https://csp-logger.vercel.app/csp-report;report-to csp-reporting-endpoint;"
}
]
},
{
"source": "/(.*)",
"missing": [
{
"type": "host",
Expand Down
60 changes: 60 additions & 0 deletions vite.base.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,63 @@
},
}
}

export type CspMode =
| 'electron'
| 'vercel-preview'
| 'vercel-production'
| 'local'

export function indexHtmlCsp(mode: CspMode): Plugin {
let csp =
[
// By default, only allow same origin.
"default-src 'self'",
// Allow inline styles and styles from the same origin. This is how we use CSS rightnow.
"style-src 'self' 'unsafe-inline'",
// Allow images from any source and inline images. We fetch user profile images from any origin.
"img-src * blob: 'unsafe-inline'",
// Allow scripts from the same origin and from Plausible Analytics. Allow WASM execution.
`script-src 'self' 'wasm-unsafe-eval' https://plausible.corp.zoo.dev/js/script.tagged-events.js${mode === 'vercel-preview' ? " https://vercel.live 'unsafe-eval'" : ''}`,
// vercel.live is used for feedback scripts in preview deployments.
...(mode === 'vercel-preview'
? ["frame-src 'self' https://vercel.live"]
: []),
// Allow WebSocket connections and fetches to our API.
"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",
// Disallow legacy stuff
"object-src 'none'",
// frame ancestors can only be blocked using HTTP headers (see vercel.json)
// "frame-ancestors 'none'",
...(mode === 'vercel-preview' || mode == 'vercel-production'
? [
"frame-ancestors 'none'",
'report-uri https://csp-logger.vercel.app/csp-report',
'report-to csp-reporting-endpoint',
]
: []),
].join(';') + ';'

console.log(
'CSP: Production build using the Content-Security-Policy (you may copy this to vercel.json when running npx vite build): ',
csp
)

return {
name: 'html-transform',
transformIndexHtml(html: string) {
let indexHtmlRegex =
/<meta\shttp-equiv="Content-Security-Policy"\scontent="(.*?)">/
if (mode === 'vercel-production' || mode == 'vercel-preview') {
// Web deployments that are deployed to vercel don't need a CSP in the indexHTML.
// They get it through vercel.json.
return html.replace(indexHtmlRegex, '')
} else {
return html.replace(
indexHtmlRegex,
`<meta http-equiv="Content-Security-Policy" content="${csp}">`

Check warning on line 172 in vite.base.config.ts

View workflow job for this annotation

GitHub Actions / semgrep-oss/scan

html-in-template-string

This template literal looks like HTML and has interpolated variables. These variables are not HTMLencoded by default. If the variables contain HTML tags these may be interpreted by the browser resulting in crosssite scripting XSS.
)
}
},
}
}
9 changes: 9 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import version from 'vite-plugin-package-version'
import topLevelAwait from 'vite-plugin-top-level-await'
import viteTsconfigPaths from 'vite-tsconfig-paths'
import { configDefaults, defineConfig } from 'vitest/config'
import { indexHtmlCsp } from './vite.base.config'

export default defineConfig(({ command, mode }) => {
const runMillion = process.env.RUN_MILLION
Expand Down Expand Up @@ -77,6 +78,14 @@ export default defineConfig(({ command, mode }) => {
},
plugins: [
react(),
indexHtmlCsp(
// production means it was build using `vite build`
mode == 'production'
? process.env.VITE_KITTYCAD_BASE_DOMAIN === 'dev.zoo.dev'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will likely be the first place logic is tied to BASE_DOMAIN so I want to make sure @nadr0 signs off on this approach.

Copy link
Contributor

@nadr0 nadr0 Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen if someone changes the BASE_DOMAIN during runtime, would these HTML headers be bricked?

Someone that builds a production binary is allowed to point the base domain to any domain. Localhost, dev, zoogov.dev and production.

If this bricks that workflow we are going to need a new approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BASE_DOMAIN is only for vercel essentially. mode == 'produciton' is my way to detect if we build on vercel right now.

So probably changing that would be required.

The CSP does not allow connecting to gov or localhost right now, so either we adapt the CSP or include them in a certain configuration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the vercel detection and also only enabled CSP for "production" builds in web

? 'vercel-preview'
: 'vercel-production'
: 'local'
),
viteTsconfigPaths(),
eslint(),
version(),
Expand Down
3 changes: 2 additions & 1 deletion vite.renderer.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { defineConfig } from 'vite'
import topLevelAwait from 'vite-plugin-top-level-await'
import viteTsconfigPaths from 'vite-tsconfig-paths'

import { pluginExposeRenderer } from './vite.base.config'
import { pluginExposeRenderer, indexHtmlCsp } from './vite.base.config'

// https://vitejs.dev/config
export default defineConfig((env) => {
Expand All @@ -23,6 +23,7 @@ export default defineConfig((env) => {
// Needed for electron-forge (in npm run tron:start)
optimizeDeps: { esbuildOptions: { target: 'es2022' } },
plugins: [
indexHtmlCsp('electron'),
pluginExposeRenderer(name),
viteTsconfigPaths(),
lezer(),
Expand Down
Loading