Skip to content
Merged
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
27 changes: 3 additions & 24 deletions src/ipc-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -88,39 +87,19 @@ 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,
];
] as const;

export type IpcMainEvent = (typeof ipcMainEvents)[number];

export const WEBCONTENTS_READY_FOR_IPC_SIGNAL =
'WEBCONTENTS_READY_FOR_IPC_SIGNAL';
53 changes: 49 additions & 4 deletions src/main/files.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as os from 'node:os';
import * as path from 'node:path';

import { BrowserWindow, IpcMainInvokeEvent, app, dialog } from 'electron';
Expand All @@ -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,
Expand Down
30 changes: 28 additions & 2 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { EventEmitter } from 'node:events';

import { MessagePortMain, ipcMain } from 'electron';
import { BrowserWindow, MessagePortMain, ipcMain } from 'electron';

import { getOrCreateMainWindow } from './windows';
import {
IpcEvents,
IpcMainEvent,
WEBCONTENTS_READY_FOR_IPC_SIGNAL,
ipcMainEvents,
} from '../ipc-events';
Expand All @@ -26,7 +27,12 @@ class IpcMainManager extends EventEmitter {

ipcMainEvents.forEach((name) => {
ipcMain.removeAllListeners(name);
ipcMain.on(name, (...args: Array<any>) => this.emit(name, ...args));
ipcMain.on(name, (event: Electron.IpcMainEvent, ...args: Array<any>) => {
// 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(
Expand All @@ -44,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.
Expand Down
20 changes: 18 additions & 2 deletions tests/main/ipc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand Down
Loading