From 8e84cbf609f301edcbf4affdf3723fb77e7b5847 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Fri, 19 Dec 2025 12:43:54 +0100 Subject: [PATCH] fix: Implement Content Security Policy and secure COOP/COEP headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add strict Content Security Policy to protect against XSS attacks - Implement custom app:// protocol for production builds with proper headers - Enable secure cross-origin isolation for SharedArrayBuffer support - Remove insecure --enable-features bypass flag - Add proper COOP/COEP/CORP headers for all resources - Allow Pyodide workers to function in production builds This fixes critical security vulnerabilities while maintaining full functionality including Pyodide web workers and SharedArrayBuffer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- electron/main.ts | 101 ++++++++++++++++++++++++++++++++++++++--------- index.html | 17 ++++++++ 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 14b8c40..cb76852 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain, dialog } from "electron"; +import { app, BrowserWindow, ipcMain, dialog, protocol } from "electron"; import { join } from "path"; import { promises as fs } from "fs"; import Store from "electron-store"; @@ -22,8 +22,73 @@ updateElectronApp({ app.commandLine.appendSwitch("enable-web-bluetooth", "true"); app.commandLine.appendSwitch("enable-experimental-web-platform-features"); +// Register custom protocol for serving app files with proper COOP/COEP headers +// This is required for SharedArrayBuffer support in production builds +protocol.registerSchemesAsPrivileged([ + { + scheme: "app", + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + corsEnabled: false, + bypassCSP: false, + }, + }, +]); + const store = new Store(); +// Setup custom protocol handler for production builds +async function setupCustomProtocol() { + protocol.handle("app", async (request) => { + // Parse the URL to get the file path + const url = new URL(request.url); + let filePath = decodeURIComponent(url.pathname); + + // Handle Windows paths (remove leading slash) + if (process.platform === "win32" && filePath.startsWith("/")) { + filePath = filePath.slice(1); + } + + // Resolve relative to app resources + const resourcePath = join(__dirname, "..", "renderer", MAIN_WINDOW_VITE_NAME, filePath); + + try { + const data = await fs.readFile(resourcePath); + + // Determine content type from extension + const ext = filePath.split(".").pop()?.toLowerCase(); + const contentTypes: Record = { + html: "text/html", + js: "application/javascript", + css: "text/css", + json: "application/json", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + svg: "image/svg+xml", + wasm: "application/wasm", + whl: "application/zip", + }; + const contentType = contentTypes[ext || ""] || "application/octet-stream"; + + // Return response with proper COOP/COEP/CORP headers + return new Response(data, { + headers: { + "Content-Type": contentType, + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", + "Cross-Origin-Resource-Policy": "same-origin", + }, + }); + } catch (err) { + console.error("[CustomProtocol] Failed to load:", resourcePath, err); + return new Response("Not Found", { status: 404 }); + } + }); +} + function createWindow() { const mainWindow = new BrowserWindow({ width: 1600, @@ -38,8 +103,7 @@ function createWindow() { contextIsolation: true, sandbox: true, webSecurity: true, - // Enable SharedArrayBuffer for Pyodide - additionalArguments: ["--enable-features=SharedArrayBuffer"], + // SharedArrayBuffer enabled via proper COOP/COEP headers below }, }); @@ -106,21 +170,16 @@ function createWindow() { // Set COOP/COEP headers for Pyodide SharedArrayBuffer support mainWindow.webContents.session.webRequest.onHeadersReceived( (details, callback) => { - // Apply headers to ALL resources including workers + // Apply security headers to enable cross-origin isolation (needed for SharedArrayBuffer) const headers: Record = { ...details.responseHeaders, - "Cross-Origin-Opener-Policy": ["unsafe-none"], - "Cross-Origin-Embedder-Policy": ["unsafe-none"], + "Cross-Origin-Opener-Policy": ["same-origin"], + "Cross-Origin-Embedder-Policy": ["require-corp"], + // Add CORP to ALL resources since this is a local-only app + // This allows workers, Pyodide assets, and all other resources to load + "Cross-Origin-Resource-Policy": ["same-origin"], }; - // For same-origin resources (including workers), add CORP header - if ( - details.url.startsWith("http://localhost") || - details.url.startsWith("file://") - ) { - headers["Cross-Origin-Resource-Policy"] = ["same-origin"]; - } - callback({ responseHeaders: headers }); }, ); @@ -131,14 +190,18 @@ function createWindow() { mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); mainWindow.webContents.openDevTools(); } else { - // MAIN_WINDOW_VITE_NAME is the renderer name from forge.config.js - mainWindow.loadFile( - join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`), - ); + // Production: Use custom protocol to serve files with proper COOP/COEP/CORP headers + // This enables SharedArrayBuffer support for Pyodide + mainWindow.loadURL("app://./index.html"); } } -app.whenReady().then(createWindow); +app.whenReady().then(async () => { + // Setup custom protocol for production builds + await setupCustomProtocol(); + + createWindow(); +}); app.on("window-all-closed", () => { if (process.platform !== "darwin") { diff --git a/index.html b/index.html index e014908..351af00 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,23 @@ + + Respira