feat: Security

This commit is contained in:
2026-06-05 18:34:01 +02:00
parent ee293d7daf
commit 45675192a5
134 changed files with 4128 additions and 446 deletions

View File

@@ -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;
});