mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* 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.
68 lines
2.5 KiB
TypeScript
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;
|
|
}
|
|
};
|
|
};
|