super-productivity/electron/ipc-handler-wrapper.ts
Johannes Millan c86e369369
fix(security): restrict renderer-supplied filesystem IPC paths (#8223)
* fix(security): canonicalize file-path-guard and restrict FILE_SYNC_* to outside userData

file-path-guard now canonicalizes via fs.realpathSync.native (deepest existing
ancestor) before the path.relative check, so symlinks and case-insensitive / 8.3
filesystem aliases can't bypass containment — this also hardens the existing
backup-dir (GHSA-x937) check, which was purely lexical and bypassable on macOS.

Adds assertPathOutside and applies it to every FILE_SYNC_* handler and
READ_LOCAL_IMAGE_AS_DATA_URL in local-file-sync.ts, so the renderer (untrusted
plugin/XSS) can no longer read/write/delete/list/inline files inside the app's
private dir (userData: settings/grants/db) while user-chosen sync folders still
work.

* fix(security): validate clipboard image basePath, fileName and copy source

ipc-handler-wrapper now uses isPathInsideDir containment (not a bare startsWith,
which accepted a sibling like <userData>-evil) and rejects any fileName that is
not a plain image basename or imageId containing separators/.. — closing the
clipboard grant-file forgery (fileName='simpleSettings'). CLIPBOARD_COPY_IMAGE_FILE
now requires an image-extension source. The supported-image-extension allowlist
is consolidated into a single SUPPORTED_IMAGE_EXTENSIONS in mime-type-mapping.
2026-06-09 18:16:22 +02:00

68 lines
2.5 KiB
TypeScript

import { app } from 'electron';
import { isPathInsideDir } from './file-path-guard';
import { SUPPORTED_IMAGE_EXTENSIONS } from './shared-with-frontend/mime-type-mapping.const';
/**
* Validates that `basePath` is within the userData directory. Uses proper
* containment (not a bare startsWith, which would accept a sibling like
* `<userData>-evil`).
*/
export const validatePathInUserData = (basePath: string): boolean =>
typeof basePath === 'string' && isPathInsideDir(app.getPath('userData'), basePath);
// A safe filename/id: a single path segment — no separators, no `..`, no NUL.
const isSafeName = (name: unknown): name is string =>
typeof name === 'string' &&
name.length > 0 &&
!name.includes('/') &&
!name.includes('\\') &&
!name.includes('..') &&
!name.includes('\0');
const isSafeImageFileName = (name: unknown): name is string =>
isSafeName(name) &&
SUPPORTED_IMAGE_EXTENSIONS.some((ext) => name.toLowerCase().endsWith(ext));
/**
* Creates a validated IPC handler with consistent error handling and optional
* path validation.
*
* SECURITY: the renderer is untrusted (a plugin or XSS payload can call any
* window.ea IPC). With `validatePath`, the handler refuses any `basePath`
* outside userData, any `fileName` that is not a plain image basename, and any
* `imageId` containing path separators/`..`. Without this a renderer could
* write e.g. `<userData>/simpleSettings` (forging the nodeExecution grant file)
* or traverse out of the images dir.
*/
export const createValidatedHandler = <TArgs extends object, TResult>(
handler: (args: TArgs) => Promise<TResult>,
options?: {
validatePath?: boolean;
errorValue?: TResult;
},
): ((event: Electron.IpcMainInvokeEvent, args: TArgs) => Promise<TResult>) => {
return async (_, args: TArgs) => {
try {
if (options?.validatePath && args && 'basePath' in args) {
const a = args as Record<string, unknown>;
if (!validatePathInUserData(a.basePath as string)) {
throw new Error('Invalid base path');
}
if ('fileName' in a && !isSafeImageFileName(a.fileName)) {
throw new Error('Invalid file name');
}
if ('imageId' in a && !isSafeName(a.imageId)) {
throw new Error('Invalid image id');
}
}
return await handler(args);
} catch (error) {
console.error(`IPC handler error:`, error);
if (options?.errorValue !== undefined) {
return options.errorValue;
}
throw error;
}
};
};