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:
undergroundwires
2023-12-18 17:30:56 +01:00
parent 940febc3e8
commit efa05f42bc
60 changed files with 939 additions and 423 deletions

View File

@@ -1,6 +1,7 @@
import { describe } from 'vitest';
import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
import { isBoolean } from '@/TypeHelpers';
describe('SanityChecks', () => {
describe('validateRuntimeSanity', () => {
@@ -42,7 +43,7 @@ function generateBooleanPermutations<T>(object: T | undefined): T[] {
const currentKey = keys[0];
const currentValue = object[currentKey];
if (typeof currentValue !== 'boolean') {
if (!isBoolean(currentValue)) {
return generateBooleanPermutations({
...object,
[currentKey]: currentValue,

View File

@@ -0,0 +1,15 @@
import { it, describe, expect } from 'vitest';
import { connectApisWithContextBridge } from '@/presentation/electron/preload/ContextBridging/ApiContextBridge';
describe('ApiContextBridge', () => {
describe('connectApisWithContextBridge', () => {
it('can provide keys and values', () => {
// arrange
const bridgeConnector = () => {};
// act
const act = () => connectApisWithContextBridge(bridgeConnector);
// assert
expect(act).to.not.throw();
});
});
});

View File

@@ -0,0 +1,66 @@
import { it, describe, expect } from 'vitest';
import { provideWindowVariables } from '@/presentation/electron/preload/ContextBridging/RendererApiProvider';
import {
isArray, isBoolean, isFunction, isNullOrUndefined, isNumber, isPlainObject, isString,
} from '@/TypeHelpers';
describe('RendererApiProvider', () => {
describe('provideWindowVariables', () => {
describe('conforms to Electron\'s context bridging requirements', () => {
// https://www.electronjs.org/docs/latest/api/context-bridge
const variables = provideWindowVariables();
Object.entries(variables).forEach(([key, value]) => {
it(`\`${key}\` conforms to allowed types for context bridging`, () => {
// act
const act = () => checkAllowedType(value);
// assert
expect(act).to.not.throw();
});
});
});
});
});
function checkAllowedType(value: unknown): void {
if (isBasicType(value)) {
return;
}
if (isArray(value)) {
checkArrayElements(value);
return;
}
if (!isPlainObject(value)) {
throw new Error(`Type error: Expected a valid object, array, or primitive type, but received type '${typeof value}'.`);
}
if (isNullOrUndefined(value)) {
throw new Error('Type error: Value is null or undefined, which is not allowed.');
}
checkObjectProperties(value);
}
function isBasicType(value: unknown): boolean {
return isString(value) || isNumber(value) || isBoolean(value) || isFunction(value);
}
function checkArrayElements(array: unknown[]): void {
array.forEach((item, index) => {
try {
checkAllowedType(item);
} catch (error) {
throw new Error(`Invalid array element at index ${index}: ${error.message}`);
}
});
}
function checkObjectProperties(obj: NonNullable<object>): void {
if (Object.keys(obj).some((key) => !isString(key))) {
throw new Error('Type error: At least one object key is not a string, which violates the allowed types.');
}
Object.entries(obj).forEach(([key, memberValue]) => {
try {
checkAllowedType(memberValue);
} catch (error) {
throw new Error(`Invalid object property '${key}': ${error.message}`);
}
});
}

View File

@@ -1,35 +0,0 @@
import { it, describe, expect } from 'vitest';
import { provideWindowVariables } from '@/presentation/electron/preload/WindowVariablesProvider';
describe('WindowVariablesProvider', () => {
describe('provideWindowVariables', () => {
describe('conforms to Electron\'s context bridging requirements', () => {
// https://www.electronjs.org/docs/latest/api/context-bridge
const variables = provideWindowVariables();
Object.entries(variables).forEach(([key, value]) => {
it(`\`${key}\` conforms to allowed types for context bridging`, () => {
expect(checkAllowedType(value)).to.equal(true);
});
});
});
});
});
function checkAllowedType(value: unknown) {
const type = typeof value;
if (['string', 'number', 'boolean', 'function'].includes(type)) {
return true;
}
if (Array.isArray(value)) {
return value.every(checkAllowedType);
}
if (type === 'object' && value !== null && value !== undefined) {
return (
// Every key should be a string
Object.keys(value).every((key) => typeof key === 'string')
// Every value should be of allowed type
&& Object.values(value).every(checkAllowedType)
);
}
return false;
}