From ef103b3f280a721b9e1818be37e714914f022e8b Mon Sep 17 00:00:00 2001 From: CodeWithMaBot <310216433+CodeWithMaBot@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:33:45 +0200 Subject: [PATCH 1/2] fix(electron): harden window open, navigation, and permissions --- electron/main.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/electron/main.ts b/electron/main.ts index 9e206f0..4c42c43 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow } from 'electron'; +import { app, BrowserWindow, shell } from 'electron'; import path from 'node:path'; const isDev = !app.isPackaged; @@ -23,9 +23,32 @@ function createWindow(): BrowserWindow { contextIsolation: true, nodeIntegration: false, sandbox: true, + allowRunningInsecureContent: false, }, }); + win.webContents.session.setPermissionRequestHandler((_webContents, _permission, callback) => { + callback(false); + }); + + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith('https:') || url.startsWith('http:')) void shell.openExternal(url); + return { action: 'deny' }; + }); + + const isAllowedNavigation = (url: string): boolean => { + if (url.startsWith('file:')) return true; + if (isDev && url.startsWith('http://localhost:4200')) return true; + return false; + }; + + win.webContents.on('will-navigate', (event, url) => { + if (!isAllowedNavigation(url)) event.preventDefault(); + }); + win.webContents.on('will-redirect', (event, url) => { + if (!isAllowedNavigation(url)) event.preventDefault(); + }); + if (isDev) { win.loadURL('http://localhost:4200'); win.webContents.openDevTools({ mode: 'detach' }); From 302179a20f58f9b55c5fa8651c97e8167d9845d5 Mon Sep 17 00:00:00 2001 From: CodeWithMaBot <310216433+CodeWithMaBot@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:50:49 +0200 Subject: [PATCH 2/2] fix(electron): validate navigation URLs by parsing instead of prefix match --- electron/main.ts | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 4c42c43..d878aa4 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,5 +1,6 @@ import { app, BrowserWindow, shell } from 'electron'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; const isDev = !app.isPackaged; @@ -36,10 +37,40 @@ function createWindow(): BrowserWindow { return { action: 'deny' }; }); + const entryFile = path.normalize(path.join(__dirname, '../dist/watch-list/browser/index.html')); + const entryDir = path.dirname(entryFile); + const isAllowedNavigation = (url: string): boolean => { - if (url.startsWith('file:')) return true; - if (isDev && url.startsWith('http://localhost:4200')) return true; - return false; + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return false; + } + if (parsed.username !== '' || parsed.password !== '') return false; + if (isDev) { + return ( + parsed.protocol === 'http:' && parsed.hostname === 'localhost' && parsed.port === '4200' + ); + } + if (parsed.protocol !== 'file:' || parsed.host !== '') return false; + let filePath: string; + try { + const fileUrl = new URL(url); + fileUrl.hash = ''; + fileUrl.search = ''; + filePath = path.normalize(fileURLToPath(fileUrl)); + } catch { + return false; + } + if (filePath === entryFile) return true; + const relative = path.relative(entryDir, filePath); + return ( + relative !== '' && + relative !== '..' && + !relative.startsWith(`..${path.sep}`) && + !path.isAbsolute(relative) + ); }; win.webContents.on('will-navigate', (event, url) => { @@ -53,7 +84,7 @@ function createWindow(): BrowserWindow { win.loadURL('http://localhost:4200'); win.webContents.openDevTools({ mode: 'detach' }); } else { - win.loadFile(path.join(__dirname, '../dist/watch-list/browser/index.html')); + win.loadFile(entryFile); } win.once('ready-to-show', () => win.show());