feat(electronSecurity): add security layer for exec commands

This commit is contained in:
Johannes Millan 2023-12-17 14:55:59 +01:00
parent e8343d060e
commit 87ad3d59b5
3 changed files with 79 additions and 11 deletions

View file

@ -2,6 +2,7 @@
// ---------------
import {
app,
dialog,
globalShortcut,
ipcMain,
IpcMainEvent,
@ -19,6 +20,7 @@ import { log } from 'electron-log/main';
import { exec } from 'child_process';
import { getWin } from './main-window';
import { quitApp, showOrFocus } from './various-shared';
import { loadSimpleStoreAll, saveSimpleStore } from './simple-store';
// HANDLER
// -------
@ -55,9 +57,6 @@ ipcMain.on(IPC.RELOAD_MAIN_WIN, () => getWin().reload());
ipcMain.on(IPC.OPEN_PATH, (ev, path: string) => shell.openPath(path));
ipcMain.on(IPC.OPEN_EXTERNAL, (ev, url: string) => shell.openExternal(url));
// TODO check
ipcMain.on(IPC.EXEC, execWithFrontendErrorHandlerInform);
ipcMain.on(IPC.LOCK_SCREEN, () => {
if ((app as any).isLocked) {
return;
@ -109,6 +108,8 @@ ipcMain.on(IPC.SHOW_OR_FOCUS, () => {
showOrFocus(mainWin);
});
ipcMain.on(IPC.EXEC, execWithFrontendErrorHandlerInform);
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function registerShowAppShortCuts(cfg: KeyboardConfig): void {
// unregister all previous
@ -179,12 +180,49 @@ function registerShowAppShortCuts(cfg: KeyboardConfig): void {
}
}
const COMMAND_MAP_PROP = 'allowedCommands';
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function execWithFrontendErrorHandlerInform(ev: IpcMainEvent, command: string): void {
log('running command ' + command);
exec(command, (err) => {
if (err) {
errorHandlerWithFrontendInform(err);
async function execWithFrontendErrorHandlerInform(
ev: IpcMainEvent,
command: string,
): Promise<void> {
log('trying to run command ' + command);
const existingData = await loadSimpleStoreAll();
const allowedCommands: string[] = (existingData[COMMAND_MAP_PROP] as string[]) || [];
if (!Array.isArray(allowedCommands)) {
throw new Error('allowedCommands is no array ???');
}
if (allowedCommands.includes(command)) {
exec(command, (err) => {
if (err) {
errorHandlerWithFrontendInform(err);
}
});
} else {
const res = await dialog.showMessageBox(null, {
type: 'question',
buttons: ['Cancel', 'Yes, execute!'],
defaultId: 2,
title: 'Super Productivity Exec',
message:
'Do you want to execute this command? ONLY confirm if you are sure you know what you are doing!!',
detail: command,
checkboxLabel: 'Remember my answer',
checkboxChecked: true,
});
const { response, checkboxChecked } = res;
if (response === 1) {
if (checkboxChecked) {
await saveSimpleStore(COMMAND_MAP_PROP, [...allowedCommands, command]);
}
exec(command, (err) => {
if (err) {
errorHandlerWithFrontendInform(err);
}
});
}
});
}
}

View file

@ -77,8 +77,7 @@ const ea: ElectronAPI = {
updateCurrentTask: (task) => _send('CURRENT_TASK_UPDATED', task),
// TODO make secure
exec: () => _send('EXEC'),
exec: (command: string) => _send('EXEC', command),
};
contextBridge.exposeInMainWorld('ea', ea);

31
electron/simple-store.ts Normal file
View file

@ -0,0 +1,31 @@
import { promises as fs } from 'fs';
import { app } from 'electron';
import * as path from 'path';
const DATA_PATH = path.join(app.getPath('userData'), 'simpleSettings');
type SimpleStoreData = { [key: string]: unknown };
export const saveSimpleStore = async (
dataKey = 'main',
data: unknown,
): Promise<unknown> => {
const prevData = await loadSimpleStoreAll();
return await fs.writeFile(DATA_PATH, JSON.stringify({ ...prevData, [dataKey]: data }), {
encoding: 'utf8',
});
};
export const loadSimpleStoreAll = async (): Promise<SimpleStoreData> => {
try {
const data = await fs.readFile(DATA_PATH, { encoding: 'utf8' });
console.log(data);
console.log(JSON.parse(data));
return JSON.parse(data);
} catch (e) {
console.error(e);
return {};
}
};