From ffb6b6b5eeb9e2df1a4fcb696cc99602cc273262 Mon Sep 17 00:00:00 2001 From: jackkav Date: Fri, 26 Jun 2026 11:36:40 +0200 Subject: [PATCH] feat(security): report-only Content-Security-Policy for renderer Electron security checklist item 7. Adds a Content-Security-Policy-Report-Only header to the index.html document served by the custom https protocol handler. Report-only mode does not block any flow; Chromium logs violations to the renderer console so the policy can be tuned from real data before a follow-up flips it to enforcing. Policy lives in content-security-policy.ts and is pinned by a unit test. --- packages/insomnia/src/main/api.protocol.ts | 17 ++++++- .../src/main/content-security-policy.test.ts | 29 +++++++++++ .../src/main/content-security-policy.ts | 49 +++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 packages/insomnia/src/main/content-security-policy.test.ts create mode 100644 packages/insomnia/src/main/content-security-policy.ts diff --git a/packages/insomnia/src/main/api.protocol.ts b/packages/insomnia/src/main/api.protocol.ts index 21d2510d75ab..4301e46d6602 100644 --- a/packages/insomnia/src/main/api.protocol.ts +++ b/packages/insomnia/src/main/api.protocol.ts @@ -7,6 +7,7 @@ import { app, net, protocol, session } from 'electron'; import { services } from 'insomnia-data'; import { getApiBaseURL } from '../common/constants'; +import { withContentSecurityPolicy } from './content-security-policy'; import { setDefaultProtocol } from './network/libcurl-promise'; import { resolveDbByKey } from './templating-worker-database'; @@ -181,10 +182,22 @@ export async function registerInsomniaProtocols() { const url = new URL(request.url); if (url.hostname === 'insomnia-app.local') { const rootDir = path.resolve(__dirname, 'client'); - const filePath = path.join(rootDir, url.pathname.startsWith('/assets') ? url.pathname : 'index.html'); + const isAsset = url.pathname.startsWith('/assets'); + const filePath = path.join(rootDir, isAsset ? url.pathname : 'index.html'); console.log(`Loading index for: ${url.pathname} from: ${filePath}`); - return await net.fetch(`file://${filePath}`, { bypassCustomProtocolHandlers: true }); + const response = await net.fetch(`file://${filePath}`, { bypassCustomProtocolHandlers: true }); + + // Apply the report-only CSP to the top-level document (index.html) only. + // Subresources inherit the document's policy. See ./content-security-policy. + if (!isAsset) { + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: withContentSecurityPolicy(response.headers), + }); + } + return response; } // Allow Google Fonts to bypass the custom https protocol handler. diff --git a/packages/insomnia/src/main/content-security-policy.test.ts b/packages/insomnia/src/main/content-security-policy.test.ts new file mode 100644 index 000000000000..8a54006347d8 --- /dev/null +++ b/packages/insomnia/src/main/content-security-policy.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; + +import { + CONTENT_SECURITY_POLICY_REPORT_ONLY, + CSP_HEADER_NAME, + withContentSecurityPolicy, +} from './content-security-policy'; + +describe('content security policy', () => { + it('ships in report-only mode so it cannot block flows', () => { + expect(CSP_HEADER_NAME).toBe('Content-Security-Policy-Report-Only'); + }); + + it('includes the core directives', () => { + expect(CONTENT_SECURITY_POLICY_REPORT_ONLY).toContain("default-src 'self'"); + expect(CONTENT_SECURITY_POLICY_REPORT_ONLY).toContain("object-src 'none'"); + expect(CONTENT_SECURITY_POLICY_REPORT_ONLY).toContain("base-uri 'self'"); + }); + + it('applies the policy onto document headers without dropping existing ones', () => { + const original = new Headers({ 'content-type': 'text/html' }); + const result = withContentSecurityPolicy(original); + + expect(result.get('content-type')).toBe('text/html'); + expect(result.get(CSP_HEADER_NAME)).toBe(CONTENT_SECURITY_POLICY_REPORT_ONLY); + // does not mutate the input + expect(original.get(CSP_HEADER_NAME)).toBeNull(); + }); +}); diff --git a/packages/insomnia/src/main/content-security-policy.ts b/packages/insomnia/src/main/content-security-policy.ts new file mode 100644 index 000000000000..80d89d037b6b --- /dev/null +++ b/packages/insomnia/src/main/content-security-policy.ts @@ -0,0 +1,49 @@ +/** + * Content-Security-Policy for the main renderer (Electron security checklist + * item 7, "Define a Content-Security-Policy"). + * + * Shipped in REPORT-ONLY mode first. The renderer is a large, complex surface + * (Monaco editor, web workers, analytics, OAuth flows, custom protocols) and an + * over-strict enforcing policy would break flows silently. Report-only does not + * block anything; instead Chromium logs each violation to the renderer console + * ("[Report Only] Refused to ..."), so the policy can be tuned from real data + * before a follow-up flips the header name to `Content-Security-Policy`. + * + * The directives below are a deliberately permissive first draft that encodes + * current intent; tighten them as reports come in. Notable allowances: + * - `style-src 'unsafe-inline'`: Tailwind, Monaco and React inject inline styles. + * - `script-src 'wasm-unsafe-eval'`: allow WebAssembly without `'unsafe-eval'`. + * - `connect-src https: wss:`: covers the Insomnia API, analytics and Sentry + * ingest; enumerate concrete hosts before enforcing. + * - custom schemes back the SSE stream and templating-worker database. + * + * This module is intentionally free of side effects so the policy can be unit + * tested (see `content-security-policy.test.ts`). + */ +export const CONTENT_SECURITY_POLICY_REPORT_ONLY = [ + "default-src 'self'", + "script-src 'self' 'wasm-unsafe-eval'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: blob: https:", + "font-src 'self' data: https://fonts.gstatic.com", + "connect-src 'self' https: wss: insomnia-event-source: insomnia-templating-worker-database:", + "worker-src 'self' blob:", + "child-src 'self' blob:", + "frame-src 'self' data:", + "object-src 'none'", + "base-uri 'self'", +].join('; '); + +/** Report-only header: surfaces violations to the console without blocking. */ +export const CSP_HEADER_NAME = 'Content-Security-Policy-Report-Only'; + +/** + * Returns a copy of the given document-response headers with the report-only + * CSP applied. CSP governs a document and all of its subresources, so this only + * needs to be set on the top-level `index.html` response. + */ +export function withContentSecurityPolicy(responseHeaders: Headers): Headers { + const headers = new Headers(responseHeaders); + headers.set(CSP_HEADER_NAME, CONTENT_SECURITY_POLICY_REPORT_ONLY); + return headers; +}