diff --git a/fluxer_desktop/src/main/IpcHandlers.ts b/fluxer_desktop/src/main/IpcHandlers.ts index 190bdde95..dc5fc9ed1 100644 --- a/fluxer_desktop/src/main/IpcHandlers.ts +++ b/fluxer_desktop/src/main/IpcHandlers.ts @@ -35,6 +35,7 @@ import {openExternalDeduped} from '@electron/main/OpenExternal'; import {getStatus as getOpenH264Status, setEnabled as setOpenH264Enabled} from '@electron/main/OpenH264Manager'; import {registerPasskeyHandlers} from '@electron/main/Passkeys'; import {getAppMetricsSnapshot, getDesktopInfo, getGpuInfo} from '@electron/main/PlatformInfo'; +import {requirePrivilegedRendererDocumentSender} from '@electron/main/PrivilegedRendererDocuments'; import {getStreamerModeCaptureAppStatus} from '@electron/main/StreamerModeProcessDetection'; import { acquireStreamingPriority, @@ -347,7 +348,8 @@ export function registerIpcHandlers(): void { ipcMain.handle('clipboard-read-text', (): string => { return clipboard.readText(); }); - ipcMain.handle('clipboard-write-file', async (_event, rawOptions: unknown): Promise => { + ipcMain.handle('clipboard-write-file', async (event, rawOptions: unknown): Promise => { + requirePrivilegedRendererDocumentSender(event, 'clipboard-write-file'); try { return await copyRemoteFileToClipboard(parseClipboardWriteFileOptions(rawOptions)); } catch (error) { @@ -355,6 +357,7 @@ export function registerIpcHandlers(): void { } }); ipcMain.handle('clipboard-paste', (event): void => { + requirePrivilegedRendererDocumentSender(event, 'clipboard-paste'); event.sender.paste(); }); ipcMain.handle( @@ -386,6 +389,7 @@ export function registerIpcHandlers(): void { defaultPath: string; }, ): Promise => { + requirePrivilegedRendererDocumentSender(event, 'download-file'); const win = BrowserWindow.fromWebContents(event.sender); if (!win) { return {success: false, error: 'No window found'}; diff --git a/fluxer_desktop/src/main/NativeScreenCapture.test.mjs b/fluxer_desktop/src/main/NativeScreenCapture.test.mjs index 095b7d597..24c9e2e77 100644 --- a/fluxer_desktop/src/main/NativeScreenCapture.test.mjs +++ b/fluxer_desktop/src/main/NativeScreenCapture.test.mjs @@ -150,6 +150,9 @@ function loadNativeScreenCapture({platform = 'linux', addon, tccStatus = 'not-de normalizeScreenCaptureDimension: (value) => value, }; } + if (specifier === './PrivilegedRendererDocuments') { + return {requirePrivilegedRendererDocumentSender: () => {}}; + } throw new Error(`Unexpected import: ${specifier}`); } diff --git a/fluxer_desktop/src/main/NativeScreenCapture.ts b/fluxer_desktop/src/main/NativeScreenCapture.ts index 017f2230b..9cd52cade 100644 --- a/fluxer_desktop/src/main/NativeScreenCapture.ts +++ b/fluxer_desktop/src/main/NativeScreenCapture.ts @@ -19,6 +19,7 @@ import type { import {ipcMain} from 'electron'; import {getTccStatus} from './MacTcc'; import {isValidStartOptions, normalizeScreenCaptureDimension} from './NativeScreenCaptureValidation'; +import {requirePrivilegedRendererDocumentSender} from './PrivilegedRendererDocuments'; const logger = createChildLogger('NativeScreenCapture'); const requireModule = createRequire(import.meta.url); @@ -944,14 +945,16 @@ export function registerNativeScreenCaptureHandlers(): void { 'native-screen-capture:get-availability', (): Promise => getNativeScreenCaptureAvailability(), ); - ipcMain.handle( - 'native-screen-capture:list-sources', - (): Promise> => listNativeScreenCaptureSources(), - ); + ipcMain.handle('native-screen-capture:list-sources', (event): Promise> => { + requirePrivilegedRendererDocumentSender(event, 'native-screen-capture:list-sources'); + return listNativeScreenCaptureSources(); + }); ipcMain.handle( 'native-screen-capture:start', - (event, options: NativeScreenCaptureStartOptions): Promise => - startNativeScreenCapture(event.sender, options), + (event, options: NativeScreenCaptureStartOptions): Promise => { + requirePrivilegedRendererDocumentSender(event, 'native-screen-capture:start'); + return startNativeScreenCapture(event.sender, options); + }, ); ipcMain.handle( 'native-screen-capture:get-diagnostics', diff --git a/fluxer_desktop/src/main/NotificationsIpc.ts b/fluxer_desktop/src/main/NotificationsIpc.ts index 20873a325..757ea8e59 100644 --- a/fluxer_desktop/src/main/NotificationsIpc.ts +++ b/fluxer_desktop/src/main/NotificationsIpc.ts @@ -7,6 +7,7 @@ import type {NotificationOptions} from '@electron/common/Types'; import {getNativeNotificationsMode} from '@electron/main/LaunchOptions'; import {resolveNotificationIcon} from '@electron/main/NotificationIcon'; import {shouldPlayNotificationSound} from '@electron/main/NotificationState'; +import {requirePrivilegedRendererDocumentSender} from '@electron/main/PrivilegedRendererDocuments'; import {type BrowserWindow, ipcMain, Notification, nativeImage} from 'electron'; const logger = createChildLogger('Notifications'); @@ -319,11 +320,12 @@ export function registerNotificationIpcHandlers(getMainWindow: () => BrowserWind ipcMain.handle( 'show-notification', async ( - _event, + event, options: NotificationOptions, ): Promise<{ id: string; }> => { + requirePrivilegedRendererDocumentSender(event, 'show-notification'); const id = getNotificationId(options); if (process.platform === 'linux') { await showLinuxNativeNotification(id, options, getMainWindow); diff --git a/fluxer_desktop/src/main/PrivilegedRendererDocuments.ts b/fluxer_desktop/src/main/PrivilegedRendererDocuments.ts new file mode 100644 index 000000000..bade5b587 --- /dev/null +++ b/fluxer_desktop/src/main/PrivilegedRendererDocuments.ts @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {isTrustedOrigin} from '@electron/main/Window'; +import type {IpcMainInvokeEvent} from 'electron'; + +export class UntrustedRendererDocumentSenderError extends Error { + public constructor(channel: string) { + super(`${channel} is only reachable from a trusted top-level renderer document`); + this.name = 'UntrustedRendererDocumentSenderError'; + } +} + +function isPrivilegedRendererDocumentSender(event: IpcMainInvokeEvent): boolean { + const frame = event.senderFrame; + if (frame == null) { + return false; + } + try { + if (frame.detached) { + return false; + } + if (frame.parent != null) { + return false; + } + return isTrustedOrigin(frame.url); + } catch { + return false; + } +} + +export function requirePrivilegedRendererDocumentSender(event: IpcMainInvokeEvent, channel: string): void { + if (!isPrivilegedRendererDocumentSender(event)) { + throw new UntrustedRendererDocumentSenderError(channel); + } +} diff --git a/fluxer_desktop/src/main/Window.ts b/fluxer_desktop/src/main/Window.ts index 8f820773c..7ee41e11d 100644 --- a/fluxer_desktop/src/main/Window.ts +++ b/fluxer_desktop/src/main/Window.ts @@ -106,7 +106,7 @@ function getOrigin(url?: string): string | null { } } -function isTrustedOrigin(url?: string): boolean { +export function isTrustedOrigin(url?: string): boolean { const origin = getOrigin(url); if (!origin) return false; if (trustedWebOrigins.has(origin)) return true;