From a7e8f20371b058f0368648b4558f241cde646cd4 Mon Sep 17 00:00:00 2001 From: John Kleinschmidt Date: Mon, 4 May 2026 12:10:19 -0400 Subject: [PATCH 1/3] chore: remove unused ipc events --- src/ipc-events.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/ipc-events.ts b/src/ipc-events.ts index 1b90a15cfe..ba20b76aae 100644 --- a/src/ipc-events.ts +++ b/src/ipc-events.ts @@ -78,7 +78,6 @@ export enum IpcEvents { export const ipcMainEvents = [ IpcEvents.SHOW_WARNING_DIALOG, - IpcEvents.LOAD_LOCAL_VERSION_FOLDER, IpcEvents.CONFIRM_QUIT, IpcEvents.SET_SHOW_ME_TEMPLATE, IpcEvents.BLOCK_ACCELERATORS, @@ -88,38 +87,16 @@ export const ipcMainEvents = [ IpcEvents.RELOAD_WINDOW, IpcEvents.SET_NATIVE_THEME, IpcEvents.SHOW_WINDOW, - IpcEvents.GET_TEMPLATE_VALUES, - IpcEvents.GET_TEMPLATE, - IpcEvents.GET_TEST_TEMPLATE, - IpcEvents.CREATE_THEME_FILE, - IpcEvents.GET_AVAILABLE_THEMES, - IpcEvents.OPEN_THEME_FOLDER, - IpcEvents.READ_THEME_FILE, IpcEvents.GET_THEME_PATH, IpcEvents.IS_DEV_MODE, - IpcEvents.NPM_ADD_MODULES, - IpcEvents.NPM_IS_PM_INSTALLED, - IpcEvents.NPM_PACKAGE_RUN, - IpcEvents.FETCH_VERSIONS, IpcEvents.GET_LATEST_STABLE, IpcEvents.GET_LOCAL_VERSION_STATE, IpcEvents.GET_OLDEST_SUPPORTED_MAJOR, IpcEvents.GET_RELEASED_VERSIONS, - IpcEvents.GET_RELEASE_INFO, - IpcEvents.GET_PROJECT_NAME, IpcEvents.GET_USERNAME, IpcEvents.PATH_EXISTS, - IpcEvents.GET_ELECTRON_TYPES, - IpcEvents.UNWATCH_ELECTRON_TYPES, - IpcEvents.GET_NODE_TYPES, - IpcEvents.CLEANUP_DIRECTORY, - IpcEvents.DELETE_USER_DATA, - IpcEvents.SAVE_FILES_TO_TEMP, - IpcEvents.START_FIDDLE, IpcEvents.STOP_FIDDLE, IpcEvents.GET_VERSION_STATE, - IpcEvents.DOWNLOAD_VERSION, - IpcEvents.REMOVE_VERSION, ]; export const WEBCONTENTS_READY_FOR_IPC_SIGNAL = From 978246b67a2850fe1cf9de1a5a6b2f4426b5bd70 Mon Sep 17 00:00:00 2001 From: John Kleinschmidt Date: Mon, 4 May 2026 12:26:08 -0400 Subject: [PATCH 2/3] chore: add security hardening to ipc events --- src/main/files.ts | 53 ++++++++++++++++++++++++++++++++++++++---- src/main/ipc.ts | 9 +++++-- tests/main/ipc.spec.ts | 20 ++++++++++++++-- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/main/files.ts b/src/main/files.ts index 3a2d5b113e..732780ba21 100644 --- a/src/main/files.ts +++ b/src/main/files.ts @@ -1,3 +1,4 @@ +import * as os from 'node:os'; import * as path from 'node:path'; import { BrowserWindow, IpcMainInvokeEvent, app, dialog } from 'electron'; @@ -11,20 +12,64 @@ import { Files } from '../interfaces'; import { IpcEvents } from '../ipc-events'; import { isSupportedFile } from '../utils/editor-utils'; +/** + * Returns true if `str` is a safe bare name (no separators, no '..'). + * Used to validate renderer-supplied data names before appending to appData. + */ +function isSafeDataName(str: unknown): str is string { + return ( + typeof str === 'string' && + str.length > 0 && + !str.includes(path.sep) && + !str.includes('/') && + !str.includes('..') && + str === path.basename(str) + ); +} + +/** + * Returns true if `dir` resolves to a path inside the OS temp directory. + * Used to ensure CLEANUP_DIRECTORY cannot reach outside tmp. + */ +function isInsideTempDir(dir: unknown): dir is string { + if (typeof dir !== 'string') return false; + const tmpDir = fs.realpathSync(os.tmpdir()); + const resolved = path.resolve(dir); + return resolved.startsWith(tmpDir + path.sep) || resolved === tmpDir; +} + /** * Ensures that we're listening to file events */ export function setupFileListeners() { - ipcMainManager.on(IpcEvents.PATH_EXISTS, (event, path: string) => { - event.returnValue = fs.existsSync(path); + ipcMainManager.on(IpcEvents.PATH_EXISTS, (event, filePath: string) => { + if (typeof filePath !== 'string') { + event.returnValue = false; + return; + } + event.returnValue = fs.existsSync(filePath); }); ipcMainManager.handle( IpcEvents.CLEANUP_DIRECTORY, - (_: IpcMainInvokeEvent, dir: string) => cleanupDirectory(dir), + (_: IpcMainInvokeEvent, dir: string) => { + if (!isInsideTempDir(dir)) { + console.warn( + `cleanupDirectory: rejected path outside temp dir: ${dir}`, + ); + return false; + } + return cleanupDirectory(dir); + }, ); ipcMainManager.handle( IpcEvents.DELETE_USER_DATA, - (_: IpcMainInvokeEvent, name: string) => deleteUserData(name), + (_: IpcMainInvokeEvent, name: string) => { + if (!isSafeDataName(name)) { + console.warn(`deleteUserData: rejected unsafe name: ${name}`); + return; + } + return deleteUserData(name); + }, ); ipcMainManager.handle( IpcEvents.SAVE_FILES_TO_TEMP, diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 92254a5db8..31a5d34bf5 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'node:events'; -import { MessagePortMain, ipcMain } from 'electron'; +import { BrowserWindow, MessagePortMain, ipcMain } from 'electron'; import { getOrCreateMainWindow } from './windows'; import { @@ -26,7 +26,12 @@ class IpcMainManager extends EventEmitter { ipcMainEvents.forEach((name) => { ipcMain.removeAllListeners(name); - ipcMain.on(name, (...args: Array) => this.emit(name, ...args)); + ipcMain.on(name, (event: Electron.IpcMainEvent, ...args: Array) => { + // Only accept messages from BrowserWindows created by the app. + // This rejects IPC from WebViews, sub-frames, or detached windows. + if (!BrowserWindow.fromWebContents(event.sender)) return; + this.emit(name, event, ...args); + }); }); ipcMain.on( diff --git a/tests/main/ipc.spec.ts b/tests/main/ipc.spec.ts index 318ac0a41b..1532eac08e 100644 --- a/tests/main/ipc.spec.ts +++ b/tests/main/ipc.spec.ts @@ -17,13 +17,29 @@ describe('IpcMainManager', () => { }); describe('emit()', () => { - it('emits an Electron IPC event', () => { + it('emits an Electron IPC event from a known BrowserWindow', () => { + vi.mocked(electron.BrowserWindow.fromWebContents).mockReturnValue( + {} as electron.BrowserWindow, + ); const mockListener = vi.fn(); + const mockEvent = { sender: {} } as Electron.IpcMainEvent; ipcMainManager.on(IpcEvents.SHOW_WARNING_DIALOG, mockListener); - electron.ipcMain.emit(IpcEvents.SHOW_WARNING_DIALOG); + electron.ipcMain.emit(IpcEvents.SHOW_WARNING_DIALOG, mockEvent); expect(mockListener).toHaveBeenCalled(); }); + + it('does not emit for senders outside a known BrowserWindow', () => { + vi.mocked(electron.BrowserWindow.fromWebContents).mockReturnValue( + null as any, + ); + const mockListener = vi.fn(); + const mockEvent = { sender: {} } as Electron.IpcMainEvent; + ipcMainManager.on(IpcEvents.SHOW_WARNING_DIALOG, mockListener); + electron.ipcMain.emit(IpcEvents.SHOW_WARNING_DIALOG, mockEvent); + + expect(mockListener).not.toHaveBeenCalled(); + }); }); describe('send()', () => { From 970671da0946cfbba492a56c95215d6659775351 Mon Sep 17 00:00:00 2001 From: David Sanders Date: Mon, 4 May 2026 15:18:19 -0400 Subject: [PATCH 3/3] chore: strongly type ipcMainManager event listeners --- src/ipc-events.ts | 4 +++- src/main/ipc.ts | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/ipc-events.ts b/src/ipc-events.ts index ba20b76aae..bcdd5d4111 100644 --- a/src/ipc-events.ts +++ b/src/ipc-events.ts @@ -97,7 +97,9 @@ export const ipcMainEvents = [ IpcEvents.PATH_EXISTS, IpcEvents.STOP_FIDDLE, IpcEvents.GET_VERSION_STATE, -]; +] as const; + +export type IpcMainEvent = (typeof ipcMainEvents)[number]; export const WEBCONTENTS_READY_FOR_IPC_SIGNAL = 'WEBCONTENTS_READY_FOR_IPC_SIGNAL'; diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 31a5d34bf5..bf1fefd62a 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -5,6 +5,7 @@ import { BrowserWindow, MessagePortMain, ipcMain } from 'electron'; import { getOrCreateMainWindow } from './windows'; import { IpcEvents, + IpcMainEvent, WEBCONTENTS_READY_FOR_IPC_SIGNAL, ipcMainEvents, } from '../ipc-events'; @@ -49,6 +50,26 @@ class IpcMainManager extends EventEmitter { ); } + override on(event: IpcMainEvent, listener: (...args: any[]) => void): this { + return super.on(event, listener); + } + + override off(event: IpcMainEvent, listener: (...args: any[]) => void): this { + return super.off(event, listener); + } + + override once(event: IpcMainEvent, listener: (...args: any[]) => void): this { + return super.once(event, listener); + } + + override emit(event: IpcMainEvent, ...args: any[]): boolean { + return super.emit(event, ...args); + } + + override removeAllListeners(event?: IpcMainEvent): this { + return super.removeAllListeners(event); + } + /** * Send an IPC message to an instance of Electron.WebContents. * If none is specified, we'll automatically go with the main window.