Improve security by isolating code execution more
This commit enhances application security against potential attacks by isolating dependencies that access the host system (like file operations) from the renderer process. It narrows the exposed functionality to script execution only, adding an extra security layer. The changes allow secure and scalable API exposure, preparing for future functionalities such as desktop notifications for script errors (#264), improved script execution handling (#296), and creating restore points (#50) in a secure and repeatable way. Changes include: - Inject `CodeRunner` into Vue components via dependency injection. - Move `CodeRunner` to the application layer as an abstraction for better domain-driven design alignment. - Refactor `SystemOperations` and related interfaces, removing the `I` prefix. - Update architecture documentation for clarity. - Update return types in `NodeSystemOperations` to match the Node APIs. - Improve `WindowVariablesProvider` integration tests for better error context. - Centralize type checks with common functions like `isArray` and `isNumber`. - Change `CodeRunner` to use `os` parameter, ensuring correct window variable injection. - Streamline API exposure to the renderer process: - Automatically bind function contexts to prevent loss of original context. - Implement a way to create facades (wrapper/proxy objects) for increased security.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import { contextBridge } from 'electron';
|
||||
import { bindObjectMethods } from './MethodContextBinder';
|
||||
import { provideWindowVariables } from './RendererApiProvider';
|
||||
|
||||
export function connectApisWithContextBridge(
|
||||
bridgeConnector: BridgeConnector = contextBridge.exposeInMainWorld,
|
||||
apiObject: object = provideWindowVariables(),
|
||||
methodContextBinder: MethodContextBinder = bindObjectMethods,
|
||||
) {
|
||||
Object.entries(apiObject).forEach(([key, value]) => {
|
||||
bridgeConnector(key, methodContextBinder(value));
|
||||
});
|
||||
}
|
||||
|
||||
export type BridgeConnector = typeof contextBridge.exposeInMainWorld;
|
||||
|
||||
export type MethodContextBinder = typeof bindObjectMethods;
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
isArray, isFunction, isNullOrUndefined, isPlainObject,
|
||||
} from '@/TypeHelpers';
|
||||
|
||||
/**
|
||||
* Binds method contexts to their original object instances and recursively processes
|
||||
* nested objects and arrays. This is particularly useful when exposing objects across
|
||||
* different contexts in Electron, such as from the main process to the renderer process
|
||||
* via the `contextBridge`.
|
||||
*
|
||||
* In Electron's context isolation environment, methods of objects passed through the
|
||||
* `contextBridge` lose their original context (`this` binding). This function ensures that
|
||||
* each method retains its binding to its original object, allowing it to work as intended
|
||||
* when invoked from the renderer process.
|
||||
*
|
||||
* This approach decouples context isolation concerns from class implementations, enabling
|
||||
* classes to operate normally without needing explicit binding or arrow functions to maintain
|
||||
* the context.
|
||||
*/
|
||||
export function bindObjectMethods<T>(obj: T): T {
|
||||
if (isNullOrUndefined(obj)) {
|
||||
return obj;
|
||||
}
|
||||
if (isPlainObject(obj)) {
|
||||
bindMethodsOfObject(obj);
|
||||
Object.values(obj).forEach((value) => {
|
||||
if (!isNullOrUndefined(value) && !isFunction(value)) {
|
||||
bindObjectMethods(value);
|
||||
}
|
||||
});
|
||||
} else if (isArray(obj)) {
|
||||
obj.forEach((item) => bindObjectMethods(item));
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function bindMethodsOfObject<T>(obj: T): T {
|
||||
const prototype = Object.getPrototypeOf(obj);
|
||||
if (!prototype) {
|
||||
return obj;
|
||||
}
|
||||
Object.getOwnPropertyNames(prototype).forEach((property) => {
|
||||
if (!prototype.hasOwnProperty.call(obj, property)) {
|
||||
return; // Skip properties not directly on the prototype
|
||||
}
|
||||
const value = obj[property];
|
||||
if (isFunction(value)) {
|
||||
(obj as object)[property] = value.bind(obj);
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { TemporaryFileCodeRunner } from '@/infrastructure/CodeRunner/TemporaryFileCodeRunner';
|
||||
import { CodeRunner } from '@/application/CodeRunner';
|
||||
import { convertPlatformToOs } from './NodeOsMapper';
|
||||
import { createSecureFacade } from './SecureFacadeCreator';
|
||||
|
||||
export function provideWindowVariables(
|
||||
createCodeRunner: CodeRunnerFactory = () => new TemporaryFileCodeRunner(),
|
||||
createLogger: LoggerFactory = () => createElectronLogger(),
|
||||
convertToOs = convertPlatformToOs,
|
||||
createApiFacade: ApiFacadeFactory = createSecureFacade,
|
||||
): WindowVariables {
|
||||
return {
|
||||
isDesktop: true,
|
||||
log: createApiFacade(createLogger(), ['info', 'debug', 'warn', 'error']),
|
||||
os: convertToOs(process.platform),
|
||||
codeRunner: createApiFacade(createCodeRunner(), ['runCode']),
|
||||
};
|
||||
}
|
||||
|
||||
export type LoggerFactory = () => Logger;
|
||||
|
||||
export type CodeRunnerFactory = () => CodeRunner;
|
||||
|
||||
export type ApiFacadeFactory = typeof createSecureFacade;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { isFunction } from '@/TypeHelpers';
|
||||
|
||||
/**
|
||||
* Creates a secure proxy for the specified object, exposing only the public properties
|
||||
* of its interface.
|
||||
*
|
||||
* This approach prevents the full exposure of the object, thereby reducing the risk
|
||||
* of unintended access or misuse. For instance, creating a facade for a class rather
|
||||
* than exposing the class itself ensures that private members and dependencies
|
||||
* (such as file access or internal state) remain encapsulated and inaccessible.
|
||||
*/
|
||||
export function createSecureFacade<T>(
|
||||
originalObject: T,
|
||||
accessibleMembers: KeyTypeCombinations<T>,
|
||||
): T {
|
||||
const facade: Partial<T> = {};
|
||||
|
||||
accessibleMembers.forEach((key: keyof T) => {
|
||||
const member = originalObject[key];
|
||||
if (isFunction(member)) {
|
||||
facade[key] = ((...args: unknown[]) => {
|
||||
return member.apply(originalObject, args);
|
||||
}) as T[keyof T];
|
||||
} else {
|
||||
facade[key] = member;
|
||||
}
|
||||
});
|
||||
|
||||
return facade as T;
|
||||
}
|
||||
|
||||
type PrependTuple<H, T extends readonly unknown[]> = H extends unknown ? T extends unknown ?
|
||||
((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never : never : never;
|
||||
type RecursionDepthControl = [
|
||||
never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
||||
];
|
||||
type AllKeyCombinations<T, U = T, N extends number = 15> = T extends unknown ?
|
||||
PrependTuple<T, Exclude<U, T> extends infer X ? {
|
||||
0: [], 1: AllKeyCombinations<X, X, RecursionDepthControl[N]>
|
||||
}[[X] extends [never] ? 0 : 1] : never> :
|
||||
never;
|
||||
type KeyTypeCombinations<T> = AllKeyCombinations<keyof T>;
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createNodeSystemOperations } from '@/infrastructure/SystemOperations/NodeSystemOperations';
|
||||
import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { convertPlatformToOs } from './NodeOsMapper';
|
||||
|
||||
export function provideWindowVariables(
|
||||
createSystem = createNodeSystemOperations,
|
||||
createLogger: () => Logger = () => createElectronLogger(),
|
||||
convertToOs = convertPlatformToOs,
|
||||
): WindowVariables {
|
||||
return {
|
||||
system: createSystem(),
|
||||
isDesktop: true,
|
||||
log: createLogger(),
|
||||
os: convertToOs(process.platform),
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
// This file is used to securely expose Electron APIs to the application.
|
||||
|
||||
import { contextBridge } from 'electron';
|
||||
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { provideWindowVariables } from './WindowVariablesProvider';
|
||||
import { connectApisWithContextBridge } from './ContextBridging/ApiContextBridge';
|
||||
|
||||
validateRuntimeSanity({
|
||||
// Validate metadata as a preventive measure for fail-fast,
|
||||
@@ -14,10 +13,7 @@ validateRuntimeSanity({
|
||||
validateWindowVariables: false,
|
||||
});
|
||||
|
||||
const windowVariables = provideWindowVariables();
|
||||
Object.entries(windowVariables).forEach(([key, value]) => {
|
||||
contextBridge.exposeInMainWorld(key, value);
|
||||
});
|
||||
connectApisWithContextBridge();
|
||||
|
||||
// Do not remove [PRELOAD_INIT]; it's a marker used in tests.
|
||||
ElectronLogger.info('[PRELOAD_INIT] Preload script successfully initialized and executed.');
|
||||
|
||||
Reference in New Issue
Block a user