feat: Security
This commit is contained in:
@@ -61,6 +61,8 @@ import {
|
||||
import { listRunningProcessNames } from '../process-list';
|
||||
import { detectActiveGame } from '../game-detection';
|
||||
import { collectAppMetricsSnapshot } from '../app-metrics';
|
||||
import { clearAllTokens } from '../api/auth-store';
|
||||
import { assertPathUnderUserData, grantPluginReadRoot, resolveReadablePath } from '../path-jail';
|
||||
|
||||
const DEFAULT_MIME_TYPE = 'application/octet-stream';
|
||||
const MAX_ACTIVE_DESKTOP_NOTIFICATIONS = 20;
|
||||
@@ -72,6 +74,19 @@ const FILE_CLIPBOARD_FORMATS = [
|
||||
'public.file-url',
|
||||
'FileNameW'
|
||||
] as const;
|
||||
|
||||
async function resolveUserDataFilePath(filePath: string): Promise<string | null> {
|
||||
return await resolveReadablePath(filePath);
|
||||
}
|
||||
|
||||
async function resolveWritableUserDataFilePath(filePath: string): Promise<string | null> {
|
||||
try {
|
||||
return await assertPathUnderUserData(filePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const MIME_TYPES_BY_EXTENSION: Record<string, string> = {
|
||||
'.7z': 'application/x-7z-compressed',
|
||||
'.aac': 'audio/aac',
|
||||
@@ -496,6 +511,10 @@ export function setupSystemHandlers(): void {
|
||||
ipcMain.handle('set-desktop-settings', async (_event, patch: Partial<DesktopSettings>) => {
|
||||
const snapshot = updateDesktopSettings(patch);
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(patch, 'allowedSignalingServers')) {
|
||||
clearAllTokens();
|
||||
}
|
||||
|
||||
await synchronizeAutoStartSetting(snapshot.autoStart);
|
||||
updateCloseToTraySetting(snapshot.closeToTray);
|
||||
await handleDesktopSettingsChanged();
|
||||
@@ -565,6 +584,12 @@ export function setupSystemHandlers(): void {
|
||||
return false;
|
||||
}
|
||||
|
||||
const scopedDestination = await resolveWritableUserDataFilePath(destinationFilePath);
|
||||
|
||||
if (!scopedDestination) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fsp.stat(sourceFilePath);
|
||||
|
||||
@@ -572,7 +597,7 @@ export function setupSystemHandlers(): void {
|
||||
return false;
|
||||
}
|
||||
|
||||
await fsp.copyFile(sourceFilePath, destinationFilePath);
|
||||
await fsp.copyFile(sourceFilePath, scopedDestination);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -580,8 +605,14 @@ export function setupSystemHandlers(): void {
|
||||
});
|
||||
|
||||
ipcMain.handle('file-exists', async (_event, filePath: string) => {
|
||||
const scopedPath = await resolveUserDataFilePath(filePath);
|
||||
|
||||
if (!scopedPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await fsp.access(filePath, fs.constants.F_OK);
|
||||
await fsp.access(scopedPath, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -589,26 +620,40 @@ export function setupSystemHandlers(): void {
|
||||
});
|
||||
|
||||
ipcMain.handle('get-file-url', async (_event, filePath: string) => {
|
||||
if (typeof filePath !== 'string' || !filePath.trim()) {
|
||||
const scopedPath = await resolveUserDataFilePath(filePath);
|
||||
|
||||
if (!scopedPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
await fsp.access(filePath, fs.constants.F_OK);
|
||||
return pathToFileURL(filePath).toString();
|
||||
await fsp.access(scopedPath, fs.constants.F_OK);
|
||||
return pathToFileURL(scopedPath).toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('read-file', async (_event, filePath: string) => {
|
||||
const data = await fsp.readFile(filePath);
|
||||
const scopedPath = await resolveUserDataFilePath(filePath);
|
||||
|
||||
if (!scopedPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await fsp.readFile(scopedPath);
|
||||
|
||||
return data.toString('base64');
|
||||
});
|
||||
|
||||
ipcMain.handle('read-file-chunk', async (_event, filePath: string, start: number, end: number) => {
|
||||
const fileHandle = await fsp.open(filePath, 'r');
|
||||
const scopedPath = await resolveUserDataFilePath(filePath);
|
||||
|
||||
if (!scopedPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileHandle = await fsp.open(scopedPath, 'r');
|
||||
|
||||
try {
|
||||
const safeStart = Math.max(0, Math.trunc(start));
|
||||
@@ -623,7 +668,13 @@ export function setupSystemHandlers(): void {
|
||||
});
|
||||
|
||||
ipcMain.handle('get-file-size', async (_event, filePath: string) => {
|
||||
const stats = await fsp.stat(filePath);
|
||||
const scopedPath = await resolveUserDataFilePath(filePath);
|
||||
|
||||
if (!scopedPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stats = await fsp.stat(scopedPath);
|
||||
|
||||
return stats.size;
|
||||
});
|
||||
@@ -632,23 +683,47 @@ export function setupSystemHandlers(): void {
|
||||
return await readClipboardFiles();
|
||||
});
|
||||
|
||||
ipcMain.handle('grant-plugin-read-root', (_event, rootPath: string) => {
|
||||
grantPluginReadRoot(rootPath);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('write-file', async (_event, filePath: string, base64Data: string) => {
|
||||
const scopedPath = await resolveWritableUserDataFilePath(filePath);
|
||||
|
||||
if (!scopedPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
await fsp.writeFile(filePath, buffer);
|
||||
await fsp.writeFile(scopedPath, buffer);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('append-file', async (_event, filePath: string, base64Data: string) => {
|
||||
const scopedPath = await resolveWritableUserDataFilePath(filePath);
|
||||
|
||||
if (!scopedPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
await fsp.appendFile(filePath, buffer);
|
||||
await fsp.appendFile(scopedPath, buffer);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-file', async (_event, filePath: string) => {
|
||||
const scopedPath = await resolveWritableUserDataFilePath(filePath);
|
||||
|
||||
if (!scopedPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await fsp.unlink(filePath);
|
||||
await fsp.unlink(scopedPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if ((error as { code?: string }).code === 'ENOENT') {
|
||||
@@ -683,7 +758,14 @@ export function setupSystemHandlers(): void {
|
||||
cancelled: false };
|
||||
}
|
||||
|
||||
const stats = await fsp.stat(sourceFilePath);
|
||||
const scopedSourcePath = await resolveUserDataFilePath(sourceFilePath);
|
||||
|
||||
if (!scopedSourcePath) {
|
||||
return { saved: false,
|
||||
cancelled: false };
|
||||
}
|
||||
|
||||
const stats = await fsp.stat(scopedSourcePath);
|
||||
|
||||
if (!stats.isFile()) {
|
||||
return { saved: false,
|
||||
@@ -691,7 +773,7 @@ export function setupSystemHandlers(): void {
|
||||
}
|
||||
|
||||
const result = await dialog.showSaveDialog({
|
||||
defaultPath: defaultFileName || path.basename(sourceFilePath)
|
||||
defaultPath: defaultFileName || path.basename(scopedSourcePath)
|
||||
});
|
||||
|
||||
if (result.canceled || !result.filePath) {
|
||||
@@ -699,7 +781,7 @@ export function setupSystemHandlers(): void {
|
||||
cancelled: true };
|
||||
}
|
||||
|
||||
await fsp.copyFile(sourceFilePath, result.filePath);
|
||||
await fsp.copyFile(scopedSourcePath, result.filePath);
|
||||
|
||||
return { saved: true,
|
||||
cancelled: false };
|
||||
@@ -711,15 +793,22 @@ export function setupSystemHandlers(): void {
|
||||
reason: 'missing-path' };
|
||||
}
|
||||
|
||||
const scopedPath = await resolveUserDataFilePath(filePath);
|
||||
|
||||
if (!scopedPath) {
|
||||
return { opened: false,
|
||||
reason: 'outside-app-data' };
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fsp.stat(filePath);
|
||||
const stats = await fsp.stat(scopedPath);
|
||||
|
||||
if (!stats.isFile()) {
|
||||
return { opened: false,
|
||||
reason: 'not-a-file' };
|
||||
}
|
||||
|
||||
const error = await shell.openPath(filePath);
|
||||
const error = await shell.openPath(scopedPath);
|
||||
|
||||
return error
|
||||
? { opened: false,
|
||||
@@ -732,7 +821,13 @@ export function setupSystemHandlers(): void {
|
||||
});
|
||||
|
||||
ipcMain.handle('ensure-dir', async (_event, dirPath: string) => {
|
||||
await fsp.mkdir(dirPath, { recursive: true });
|
||||
const scopedPath = await resolveWritableUserDataFilePath(dirPath);
|
||||
|
||||
if (!scopedPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await fsp.mkdir(scopedPath, { recursive: true });
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user