Compare commits

...

1 Commits

Author SHA1 Message Date
Brendan Allan 0e1bedda1b fix(desktop): restrict renderer navigation 2026-07-26 03:19:04 +00:00
3 changed files with 46 additions and 0 deletions
@@ -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)
})
})
@@ -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
}
+14
View File
@@ -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)