diff --git a/AgentCompany-New/src/main/app/external-url.ts b/AgentCompany-New/src/main/app/external-url.ts new file mode 100644 index 00000000..4c225249 --- /dev/null +++ b/AgentCompany-New/src/main/app/external-url.ts @@ -0,0 +1,6 @@ +export function normalizeExternalBrowserUrl(value: string): string { + const url = new URL(value); + if (url.protocol !== "https:") throw new Error("External URLs must use HTTPS."); + if (url.username || url.password) throw new Error("External URLs must not include credentials."); + return url.toString(); +} diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index 3a0e5328..110f5156 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -27,6 +27,7 @@ import { KnowledgeService } from "../services/knowledge-service"; import { AutomationService } from "../services/automation-service"; import { MeetingService } from "../services/meeting-service"; import { SprintService } from "../services/sprint-service"; +import { normalizeExternalBrowserUrl } from "./external-url"; export interface IpcDeps { connection: DatabaseConnection; @@ -177,8 +178,9 @@ export function registerIpcHandlers(deps: IpcDeps): void { { providerKey: input.providerKey, enterpriseDomain: input.enterpriseDomain ?? null }, { onAuth: ({ url, instructions }) => { - void shell.openExternal(url); - emit({ phase: "authorize", url, instructions }); + const browserUrl = normalizeExternalBrowserUrl(url); + void shell.openExternal(browserUrl); + emit({ phase: "authorize", url: browserUrl, instructions }); }, onProgress: (message) => emit({ phase: "progress", message }), }, @@ -209,11 +211,7 @@ export function registerIpcHandlers(deps: IpcDeps): void { }); ipcMain.handle(IPC_CHANNELS.openExternalUrl, async (_event, url: string) => { try { - const parsed = new URL(url); - if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { - return fail("external_url_blocked", "Only http(s) URLs may be opened externally."); - } - await shell.openExternal(parsed.toString()); + await shell.openExternal(normalizeExternalBrowserUrl(url)); return ok(null); } catch (error) { return fail("external_url_failed", error instanceof Error ? error.message : String(error)); diff --git a/AgentCompany-New/tests/unit/external-url.test.ts b/AgentCompany-New/tests/unit/external-url.test.ts new file mode 100644 index 00000000..863393b6 --- /dev/null +++ b/AgentCompany-New/tests/unit/external-url.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from "vitest"; +import { normalizeExternalBrowserUrl } from "../../src/main/app/external-url"; + +describe("external browser URL validation", () => { + test("normalizes HTTPS URLs and preserves query strings", () => { + expect(normalizeExternalBrowserUrl("https://example.com/docs?q=agent")).toBe("https://example.com/docs?q=agent"); + }); + + test("rejects non-HTTPS and credential-bearing URLs", () => { + expect(() => normalizeExternalBrowserUrl("http://example.com")).toThrow("External URLs must use HTTPS."); + expect(() => normalizeExternalBrowserUrl("mailto:security@example.com")).toThrow("External URLs must use HTTPS."); + expect(() => normalizeExternalBrowserUrl("https://user:token@example.com")).toThrow("External URLs must not include credentials."); + }); +});