From 2ab001b18a91c6f100ed2e34c1931f37c1eb172b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 15 May 2026 02:56:25 -0400 Subject: [PATCH] fix(security): require https for trusted links --- src/main/external-url-policy.ts | 2 +- tests/unit/external-url-policy.test.ts | 1 + .../unit/ipc-open-work-product-url-policy.test.ts | 14 +++++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/external-url-policy.ts b/src/main/external-url-policy.ts index 18c3dc41..4f85ac7f 100644 --- a/src/main/external-url-policy.ts +++ b/src/main/external-url-policy.ts @@ -47,7 +47,7 @@ export function classifyExternalUrl(rawUrl: string): ExternalUrlDecision { } const parsed = new URL(normalized); const host = parsed.hostname.toLowerCase(); - if (TRUSTED_EXTERNAL_HOSTS.has(host)) { + if (parsed.protocol === "https:" && TRUSTED_EXTERNAL_HOSTS.has(host)) { return { kind: "trusted", url: normalized, host }; } return { kind: "confirm", url: normalized, host }; diff --git a/tests/unit/external-url-policy.test.ts b/tests/unit/external-url-policy.test.ts index 6e4db9d8..16ff38a9 100644 --- a/tests/unit/external-url-policy.test.ts +++ b/tests/unit/external-url-policy.test.ts @@ -17,6 +17,7 @@ describe("external URL policy", () => { it("separates trusted hosts from confirmation hosts", () => { expect(classifyExternalUrl("https://github.com/acme/repo")).toMatchObject({ kind: "trusted", host: "github.com" }); + expect(classifyExternalUrl("http://github.com/acme/repo")).toMatchObject({ kind: "confirm", host: "github.com" }); expect(classifyExternalUrl("https://example.com/report")).toMatchObject({ kind: "confirm", host: "example.com" }); }); }); diff --git a/tests/unit/ipc-open-work-product-url-policy.test.ts b/tests/unit/ipc-open-work-product-url-policy.test.ts index b5c0516a..50b6d0cd 100644 --- a/tests/unit/ipc-open-work-product-url-policy.test.ts +++ b/tests/unit/ipc-open-work-product-url-policy.test.ts @@ -87,7 +87,19 @@ describe("openWorkProduct external URL policy", () => { expect(electron.openExternal).toHaveBeenCalledWith("https://github.com/acme/repo"); }); - it("asks before opening untrusted http hosts", async () => { + it("asks before opening trusted hosts over http", async () => { + electron.openExternal.mockClear(); + electron.showMessageBox.mockClear(); + electron.showMessageBox.mockResolvedValueOnce({ response: 0 }); + + const result = await openUrl("http://github.com/acme/repo"); + + expect(result).toEqual({ ok: true, data: true }); + expect(electron.showMessageBox).toHaveBeenCalledTimes(1); + expect(electron.openExternal).toHaveBeenCalledWith("http://github.com/acme/repo"); + }); + + it("asks before opening untrusted hosts", async () => { electron.openExternal.mockClear(); electron.showMessageBox.mockClear(); electron.showMessageBox.mockResolvedValueOnce({ response: 0 });