Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
17 changes: 15 additions & 2 deletions packages/insomnia/src/main/api.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
Expand Down
29 changes: 29 additions & 0 deletions packages/insomnia/src/main/content-security-policy.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
49 changes: 49 additions & 0 deletions packages/insomnia/src/main/content-security-policy.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading