Improve desktop security by isolating Electron
Enable `contextIsolation` in Electron to securely expose a limited set of Node.js APIs to the renderer process. It: 1. Isolates renderer and main process contexts. It ensures that the powerful main process functions aren't directly accessible from renderer process(es), adding a security boundary. 2. Mitigates remote exploitation risks. By isolating contexts, potential malicious code injections in the renderer can't directly reach and compromise the main process. 3. Reduces attack surface. 4. Protect against prototype pollution: It prevents tampering of JavaScript object prototypes in one context from affecting another context, improving app reliability and security. Supporting changes include: - Extract environment and system operations classes to the infrastructure layer. This removes node dependencies from core domain and application code. - Introduce `ISystemOperations` to encapsulate OS interactions. Use it from `CodeRunner` to isolate node API usage. - Add a preloader script to inject validated environment variables into renderer context. This keeps Electron integration details encapsulated. - Add new sanity check to fail fast on issues with preloader injected variables. - Improve test coverage of runtime sanity checks and environment components. Move validation logic into separate classes for Single Responsibility. - Improve absent value test case generation.
This commit is contained in:
8
tests/unit/shared/Stubs/BrowserOsDetectorStub.ts
Normal file
8
tests/unit/shared/Stubs/BrowserOsDetectorStub.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IBrowserOsDetector } from '@/infrastructure/Environment/BrowserOs/IBrowserOsDetector';
|
||||
|
||||
export class BrowserOsDetectorStub implements IBrowserOsDetector {
|
||||
public detect(): OperatingSystem {
|
||||
return OperatingSystem.BlackBerryTabletOS;
|
||||
}
|
||||
}
|
||||
13
tests/unit/shared/Stubs/CommandOpsStub.ts
Normal file
13
tests/unit/shared/Stubs/CommandOpsStub.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ICommandOps } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class CommandOpsStub
|
||||
extends StubWithObservableMethodCalls<ICommandOps>
|
||||
implements ICommandOps {
|
||||
public execute(command: string): void {
|
||||
this.registerMethodCall({
|
||||
methodName: 'execute',
|
||||
args: [command],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,22 @@
|
||||
import { IEnvironment } from '@/application/Environment/IEnvironment';
|
||||
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
|
||||
import { SystemOperationsStub } from './SystemOperationsStub';
|
||||
|
||||
export class EnvironmentStub implements IEnvironment {
|
||||
public isDesktop = true;
|
||||
|
||||
public os = OperatingSystem.Windows;
|
||||
|
||||
public withOs(os: OperatingSystem): EnvironmentStub {
|
||||
public system: ISystemOperations = new SystemOperationsStub();
|
||||
|
||||
public withOs(os: OperatingSystem): this {
|
||||
this.os = os;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSystemOperations(system: ISystemOperations): this {
|
||||
this.system = system;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
30
tests/unit/shared/Stubs/FileSystemOpsStub.ts
Normal file
30
tests/unit/shared/Stubs/FileSystemOpsStub.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { IFileSystemOps } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class FileSystemOpsStub
|
||||
extends StubWithObservableMethodCalls<IFileSystemOps>
|
||||
implements IFileSystemOps {
|
||||
public setFilePermissions(filePath: string, mode: string | number): Promise<void> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'setFilePermissions',
|
||||
args: [filePath, mode],
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public createDirectory(directoryPath: string, isRecursive?: boolean): Promise<string> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'createDirectory',
|
||||
args: [directoryPath, isRecursive],
|
||||
});
|
||||
return Promise.resolve(directoryPath);
|
||||
}
|
||||
|
||||
public writeToFile(filePath: string, data: string): Promise<void> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'writeToFile',
|
||||
args: [filePath, data],
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
40
tests/unit/shared/Stubs/LocationOpsStub.ts
Normal file
40
tests/unit/shared/Stubs/LocationOpsStub.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ILocationOps } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class LocationOpsStub
|
||||
extends StubWithObservableMethodCalls<ILocationOps>
|
||||
implements ILocationOps {
|
||||
private sequence = new Array<string>();
|
||||
|
||||
private scenarios = new Map<string, string>();
|
||||
|
||||
public withJoinResult(returnValue: string, ...paths: string[]): this {
|
||||
this.scenarios.set(LocationOpsStub.getScenarioKey(paths), returnValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withJoinResultSequence(...valuesToReturn: string[]): this {
|
||||
this.sequence.push(...valuesToReturn);
|
||||
this.sequence.reverse();
|
||||
return this;
|
||||
}
|
||||
|
||||
public combinePaths(...pathSegments: string[]): string {
|
||||
this.registerMethodCall({
|
||||
methodName: 'combinePaths',
|
||||
args: pathSegments,
|
||||
});
|
||||
if (this.sequence.length > 0) {
|
||||
return this.sequence.pop();
|
||||
}
|
||||
const key = LocationOpsStub.getScenarioKey(pathSegments);
|
||||
if (!this.scenarios.has(key)) {
|
||||
return pathSegments.join('/PATH-SEGMENT-SEPARATOR/');
|
||||
}
|
||||
return this.scenarios.get(key);
|
||||
}
|
||||
|
||||
private static getScenarioKey(paths: string[]): string {
|
||||
return paths.join('|');
|
||||
}
|
||||
}
|
||||
21
tests/unit/shared/Stubs/OperatingSystemOpsStub.ts
Normal file
21
tests/unit/shared/Stubs/OperatingSystemOpsStub.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { IOperatingSystemOps } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class OperatingSystemOpsStub
|
||||
extends StubWithObservableMethodCalls<IOperatingSystemOps>
|
||||
implements IOperatingSystemOps {
|
||||
private temporaryDirectory = '/stub-temp-dir/';
|
||||
|
||||
public withTemporaryDirectoryResult(directory: string): this {
|
||||
this.temporaryDirectory = directory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getTempDirectory(): string {
|
||||
this.registerMethodCall({
|
||||
methodName: 'getTempDirectory',
|
||||
args: [],
|
||||
});
|
||||
return this.temporaryDirectory;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/ISanityCheckOptions';
|
||||
import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
|
||||
|
||||
export class SanityCheckOptionsStub implements ISanityCheckOptions {
|
||||
public validateEnvironment = false;
|
||||
|
||||
public validateMetadata = false;
|
||||
|
||||
public withValidateMetadata(value: boolean): this {
|
||||
this.validateMetadata = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withValidateEnvironment(value: boolean): this {
|
||||
this.validateEnvironment = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/ISanityCheckOptions';
|
||||
import { ISanityValidator } from '@/infrastructure/RuntimeSanity/ISanityValidator';
|
||||
import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
|
||||
import { ISanityValidator } from '@/infrastructure/RuntimeSanity/Common/ISanityValidator';
|
||||
|
||||
export class SanityValidatorStub implements ISanityValidator {
|
||||
public shouldValidateArgs = new Array<ISanityCheckOptions>();
|
||||
|
||||
public name = 'sanity-validator-stub';
|
||||
|
||||
private errors: readonly string[] = [];
|
||||
|
||||
private shouldValidateResult = true;
|
||||
@@ -17,6 +19,11 @@ export class SanityValidatorStub implements ISanityValidator {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
public withName(name: string): this {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withErrorsResult(errors: readonly string[]): this {
|
||||
this.errors = errors;
|
||||
return this;
|
||||
|
||||
25
tests/unit/shared/Stubs/StubWithObservableMethodCalls.ts
Normal file
25
tests/unit/shared/Stubs/StubWithObservableMethodCalls.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { FunctionKeys } from '@/TypeHelpers';
|
||||
|
||||
export abstract class StubWithObservableMethodCalls<T> {
|
||||
public readonly callHistory = new Array<MethodCall<T>>();
|
||||
|
||||
public get methodCalls(): IEventSource<MethodCall<T>> {
|
||||
return this.notifiableMethodCalls;
|
||||
}
|
||||
|
||||
private readonly notifiableMethodCalls = new EventSource<MethodCall<T>>();
|
||||
|
||||
protected registerMethodCall(name: MethodCall<T>) {
|
||||
this.callHistory.push(name);
|
||||
this.notifiableMethodCalls.notify(name);
|
||||
}
|
||||
}
|
||||
|
||||
type MethodCall<T> = {
|
||||
[K in FunctionKeys<T>]: {
|
||||
methodName: K;
|
||||
args: T[K] extends (...args: infer A) => unknown ? A : never;
|
||||
}
|
||||
}[FunctionKeys<T>];
|
||||
41
tests/unit/shared/Stubs/SystemOperationsStub.ts
Normal file
41
tests/unit/shared/Stubs/SystemOperationsStub.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
ICommandOps,
|
||||
IFileSystemOps,
|
||||
IOperatingSystemOps,
|
||||
ILocationOps,
|
||||
ISystemOperations,
|
||||
} from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
|
||||
import { CommandOpsStub } from './CommandOpsStub';
|
||||
import { FileSystemOpsStub } from './FileSystemOpsStub';
|
||||
import { LocationOpsStub } from './LocationOpsStub';
|
||||
import { OperatingSystemOpsStub } from './OperatingSystemOpsStub';
|
||||
|
||||
export class SystemOperationsStub implements ISystemOperations {
|
||||
public operatingSystem: IOperatingSystemOps = new OperatingSystemOpsStub();
|
||||
|
||||
public location: ILocationOps = new LocationOpsStub();
|
||||
|
||||
public fileSystem: IFileSystemOps = new FileSystemOpsStub();
|
||||
|
||||
public command: ICommandOps = new CommandOpsStub();
|
||||
|
||||
public withOperatingSystem(operatingSystemOps: IOperatingSystemOps): this {
|
||||
this.operatingSystem = operatingSystemOps;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withLocation(location: ILocationOps): this {
|
||||
this.location = location;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFileSystem(fileSystem: IFileSystemOps): this {
|
||||
this.fileSystem = fileSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCommand(command: ICommandOps): this {
|
||||
this.command = command;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,24 @@
|
||||
import { it } from 'vitest';
|
||||
|
||||
export function itEachAbsentStringValue(runner: (absentValue: string) => void): void {
|
||||
itEachAbsentTestCase(AbsentStringTestCases, runner);
|
||||
export function itEachAbsentStringValue(
|
||||
runner: (absentValue: string) => void,
|
||||
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
|
||||
): void {
|
||||
itEachAbsentTestCase(getAbsentStringTestCases(options), runner);
|
||||
}
|
||||
|
||||
export function itEachAbsentObjectValue(
|
||||
runner: (absentValue: AbsentObjectType) => void,
|
||||
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
|
||||
): void {
|
||||
itEachAbsentTestCase(AbsentObjectTestCases, runner);
|
||||
itEachAbsentTestCase(getAbsentObjectTestCases(options), runner);
|
||||
}
|
||||
|
||||
export function itEachAbsentCollectionValue<T>(runner: (absentValue: []) => void): void {
|
||||
itEachAbsentTestCase(getAbsentCollectionTestCases<T>(), runner);
|
||||
export function itEachAbsentCollectionValue<T>(
|
||||
runner: (absentValue: []) => void,
|
||||
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
|
||||
): void {
|
||||
itEachAbsentTestCase(getAbsentCollectionTestCases<T>(options), runner);
|
||||
}
|
||||
|
||||
export function itEachAbsentTestCase<T>(
|
||||
@@ -25,28 +32,40 @@ export function itEachAbsentTestCase<T>(
|
||||
}
|
||||
}
|
||||
|
||||
export const AbsentObjectTestCases: readonly IAbsentTestCase<AbsentObjectType>[] = [
|
||||
{
|
||||
valueName: 'undefined',
|
||||
absentValue: undefined,
|
||||
},
|
||||
{
|
||||
valueName: 'null',
|
||||
absentValue: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const AbsentStringTestCases: readonly IAbsentStringCase[] = [
|
||||
{
|
||||
valueName: 'empty',
|
||||
absentValue: '',
|
||||
},
|
||||
...AbsentObjectTestCases,
|
||||
];
|
||||
|
||||
export function getAbsentCollectionTestCases<T>(): readonly IAbsentCollectionCase<T>[] {
|
||||
export function getAbsentObjectTestCases(
|
||||
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
|
||||
): IAbsentTestCase<AbsentObjectType>[] {
|
||||
return [
|
||||
...AbsentObjectTestCases,
|
||||
{
|
||||
valueName: 'null',
|
||||
absentValue: null,
|
||||
},
|
||||
...(options.excludeUndefined ? [] : [
|
||||
{
|
||||
valueName: 'undefined',
|
||||
absentValue: undefined,
|
||||
},
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
export function getAbsentStringTestCases(
|
||||
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
|
||||
): IAbsentStringCase[] {
|
||||
return [
|
||||
{
|
||||
valueName: 'empty',
|
||||
absentValue: '',
|
||||
},
|
||||
...getAbsentObjectTestCases(options),
|
||||
];
|
||||
}
|
||||
|
||||
export function getAbsentCollectionTestCases<T>(
|
||||
options: IAbsentTestCaseOptions = DefaultAbsentTestCaseOptions,
|
||||
): readonly IAbsentCollectionCase<T>[] {
|
||||
return [
|
||||
...getAbsentObjectTestCases(options),
|
||||
{
|
||||
valueName: 'empty',
|
||||
absentValue: new Array<T>(),
|
||||
@@ -54,6 +73,14 @@ export function getAbsentCollectionTestCases<T>(): readonly IAbsentCollectionCas
|
||||
];
|
||||
}
|
||||
|
||||
const DefaultAbsentTestCaseOptions: IAbsentTestCaseOptions = {
|
||||
excludeUndefined: false,
|
||||
};
|
||||
|
||||
interface IAbsentTestCaseOptions {
|
||||
readonly excludeUndefined: boolean;
|
||||
}
|
||||
|
||||
type AbsentObjectType = undefined | null;
|
||||
|
||||
interface IAbsentTestCase<T> {
|
||||
|
||||
Reference in New Issue
Block a user