From 0e1bedda1b7a83b517c6222d26a26bc0ebde04eb Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:19:04 +0000 Subject: [PATCH] fix(desktop): restrict renderer navigation --- .../src/main/navigation-policy.test.ts | 22 +++++++++++++++++++ .../desktop/src/main/navigation-policy.ts | 10 +++++++++ packages/desktop/src/main/windows.ts | 14 ++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 packages/desktop/src/main/navigation-policy.test.ts create mode 100644 packages/desktop/src/main/navigation-policy.ts diff --git a/packages/desktop/src/main/navigation-policy.test.ts b/packages/desktop/src/main/navigation-policy.test.ts new file mode 100644 index 0000000000..6778e9c892 --- /dev/null +++ b/packages/desktop/src/main/navigation-policy.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "bun:test" +import { isTrustedNavigationUrl } from "./navigation-policy" + +describe("isTrustedNavigationUrl", () => { + test("allows packaged renderer pages", () => { + expect(isTrustedNavigationUrl("oc://renderer/index.html", undefined)).toBe(true) + expect(isTrustedNavigationUrl("oc://renderer/assets/index.js", undefined)).toBe(true) + }) + + test("rejects other hosts and protocols", () => { + expect(isTrustedNavigationUrl("oc://attacker/index.html", undefined)).toBe(false) + expect(isTrustedNavigationUrl("https://example.com", undefined)).toBe(false) + expect(isTrustedNavigationUrl("not a url", undefined)).toBe(false) + }) + + test("allows only the configured development origin", () => { + const devUrl = "http://localhost:5173" + expect(isTrustedNavigationUrl("http://localhost:5173/index.html", devUrl)).toBe(true) + expect(isTrustedNavigationUrl("http://localhost:5174/index.html", devUrl)).toBe(false) + expect(isTrustedNavigationUrl("https://localhost:5173/index.html", devUrl)).toBe(false) + }) +}) diff --git a/packages/desktop/src/main/navigation-policy.ts b/packages/desktop/src/main/navigation-policy.ts new file mode 100644 index 0000000000..26ac994e86 --- /dev/null +++ b/packages/desktop/src/main/navigation-policy.ts @@ -0,0 +1,10 @@ +const rendererProtocol = "oc:" +const rendererHost = "renderer" + +export function isTrustedNavigationUrl(value: string, devUrl = process.env.ELECTRON_RENDERER_URL) { + if (!URL.canParse(value)) return false + const url = new URL(value) + if (url.protocol === rendererProtocol && url.host === rendererHost) return true + if (!devUrl || !URL.canParse(devUrl)) return false + return url.origin === new URL(devUrl).origin +} diff --git a/packages/desktop/src/main/windows.ts b/packages/desktop/src/main/windows.ts index 7a594b5302..02c0b629f1 100644 --- a/packages/desktop/src/main/windows.ts +++ b/packages/desktop/src/main/windows.ts @@ -9,6 +9,7 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path" import { fileURLToPath, pathToFileURL } from "node:url" import type { TitlebarTheme } from "../preload/types" import { exportDebugLogs, write as writeLog } from "./logging" +import { isTrustedNavigationUrl } from "./navigation-policy" import { getStore, removeStoreFile } from "./store" import { PINCH_ZOOM_ENABLED_KEY, WINDOW_IDS_KEY } from "./store-keys" import { createUnresponsiveSampler } from "./unresponsive" @@ -203,6 +204,7 @@ export function createMainWindow(id: string = randomUUID()) { }) allowRendererPermissions(win) + restrictNavigation(win) wireWindowRecovery(win, id) win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { @@ -229,6 +231,18 @@ export function createMainWindow(id: string = randomUUID()) { return win } +function restrictNavigation(win: BrowserWindow) { + win.webContents.on("will-navigate", (event, url) => { + if (isTrustedNavigationUrl(url)) return + event.preventDefault() + writeLog("window", "blocked renderer navigation", { url }, "warn") + }) + win.webContents.setWindowOpenHandler(({ url }) => { + writeLog("window", "blocked renderer window", { url }, "warn") + return { action: "deny" } + }) +} + function registerWindow(win: BrowserWindow, id: string) { windowIDs.set(win, id) registry.register(id, win)