Compare commits

...

1 Commits

Author SHA1 Message Date
Brendan Allan c552cee37e fix(desktop): restrict external links 2026-07-26 03:19:04 +00:00
3 changed files with 31 additions and 1 deletions
@@ -0,0 +1,19 @@
import { describe, expect, test } from "bun:test"
import { safeExternalUrl } from "./external-link"
describe("safeExternalUrl", () => {
test("allows web links", () => {
expect(safeExternalUrl("https://opencode.ai/docs")).toBe("https://opencode.ai/docs")
expect(safeExternalUrl("http://localhost:3000/callback")).toBe("http://localhost:3000/callback")
})
test("rejects executable and application protocols", () => {
expect(safeExternalUrl("file:///tmp/script.sh")).toBeUndefined()
expect(safeExternalUrl("ssh://attacker.example")).toBeUndefined()
expect(safeExternalUrl("custom-handler:payload")).toBeUndefined()
})
test("rejects malformed URLs", () => {
expect(safeExternalUrl("not a url")).toBeUndefined()
})
})
@@ -0,0 +1,8 @@
const allowedProtocols = new Set(["https:", "http:"])
export function safeExternalUrl(value: string) {
if (!URL.canParse(value)) return
const url = new URL(value)
if (!allowedProtocols.has(url.protocol)) return
return url.toString()
}
+4 -1
View File
@@ -8,6 +8,7 @@ import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu"
import type { FatalRendererError, ServerReadyData, TitlebarTheme } from "../preload/types"
import { runDesktopMenuAction } from "./desktop-menu-actions"
import { setForceFocus } from "./debug"
import { safeExternalUrl } from "./external-link"
import { assertAttachmentBudget, createPickedFileAuthorizations } from "./attachment-picker"
import { getStore, removeStoreFileIfEmpty } from "./store"
import { getPinchZoomEnabled, getWindowID, setPinchZoomEnabled, setTitlebar, updateTitlebar } from "./windows"
@@ -178,7 +179,9 @@ export function registerIpcHandlers(deps: Deps) {
)
ipcMain.on("open-link", (_event: IpcMainEvent, url: string) => {
void shell.openExternal(url)
const externalUrl = safeExternalUrl(url)
if (!externalUrl) return
void shell.openExternal(externalUrl)
})
ipcMain.handle("open-path", async (_event: IpcMainInvokeEvent, path: string, app?: string) => {