diff --git a/src/main/security-policy.ts b/src/main/security-policy.ts index 66e30f3..062a7ef 100644 --- a/src/main/security-policy.ts +++ b/src/main/security-policy.ts @@ -1,7 +1,6 @@ -export function isTrustedAppUrl(rawUrl: string): boolean { +function isHarnessUrl(rawUrl: string): boolean { try { const url = new URL(rawUrl) - if (url.protocol === 'file:') return true return ( url.protocol === 'http:' && (url.hostname === '127.0.0.1' || url.hostname === 'localhost') @@ -10,3 +9,25 @@ export function isTrustedAppUrl(rawUrl: string): boolean { return false } } + +export function isTrustedAppUrl(rawUrl: string): boolean { + try { + if (new URL(rawUrl).protocol === 'file:') return true + } catch { + return false + } + return isHarnessUrl(rawUrl) +} + +export function canGrantWindowPermission( + permission: string, + requestingUrl: string | undefined, + isMainFrame: boolean +): boolean { + return ( + permission === 'clipboard-sanitized-write' && + isMainFrame && + requestingUrl !== undefined && + isHarnessUrl(requestingUrl) + ) +} diff --git a/src/main/security.ts b/src/main/security.ts index 742acf1..fae4178 100644 --- a/src/main/security.ts +++ b/src/main/security.ts @@ -1,5 +1,5 @@ import { shell, type BrowserWindow } from 'electron' -import { isTrustedAppUrl } from './security-policy' +import { canGrantWindowPermission, isTrustedAppUrl } from './security-policy' export function secureWindow(window: BrowserWindow): void { window.webContents.setWindowOpenHandler(({ url }) => { @@ -15,7 +15,19 @@ export function secureWindow(window: BrowserWindow): void { }) window.webContents.on('will-attach-webview', (event) => event.preventDefault()) - window.webContents.session.setPermissionRequestHandler((_webContents, _permission, callback) => { - callback(false) - }) + window.webContents.session.setPermissionCheckHandler( + (_webContents, permission, requestingOrigin, details) => + canGrantWindowPermission( + permission, + details.requestingUrl ?? requestingOrigin, + details.isMainFrame + ) + ) + window.webContents.session.setPermissionRequestHandler( + (_webContents, permission, callback, details) => { + callback( + canGrantWindowPermission(permission, details.requestingUrl, details.isMainFrame) + ) + } + ) } diff --git a/test/runtime.test.ts b/test/runtime.test.ts index 2ece421..83f5331 100644 --- a/test/runtime.test.ts +++ b/test/runtime.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { buildHarnessArguments, buildNodeArguments } from '../src/main/runtime/harness-runtime' -import { isTrustedAppUrl } from '../src/main/security-policy' +import { canGrantWindowPermission, isTrustedAppUrl } from '../src/main/security-policy' import { shouldLoadHarnessUrl } from '../src/main/window-navigation' describe('Harness launch contract', () => { @@ -36,6 +36,43 @@ describe('navigation trust boundary', () => { expect(isTrustedAppUrl('http://example.com')).toBe(false) expect(isTrustedAppUrl('javascript:alert(1)')).toBe(false) }) + + it('only grants clipboard writes from the trusted main frame', () => { + expect( + canGrantWindowPermission( + 'clipboard-sanitized-write', + 'http://127.0.0.1:43127/session', + true + ) + ).toBe(true) + expect( + canGrantWindowPermission( + 'clipboard-sanitized-write', + 'http://localhost:43127/session', + true + ) + ).toBe(true) + expect( + canGrantWindowPermission('clipboard-read', 'http://127.0.0.1:43127/session', true) + ).toBe(false) + expect( + canGrantWindowPermission( + 'clipboard-sanitized-write', + 'http://127.0.0.1:43127/session', + false + ) + ).toBe(false) + expect( + canGrantWindowPermission( + 'clipboard-sanitized-write', + 'https://example.com/session', + true + ) + ).toBe(false) + expect( + canGrantWindowPermission('clipboard-sanitized-write', 'file:///tmp/app.html', true) + ).toBe(false) + }) }) describe('Harness window activation', () => {