From 3e975a971fd88094e2c20c2bf445eb2bcd898624 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 9 May 2025 14:26:54 +0000 Subject: [PATCH 1/3] refactor: add external apps protocol safe list --- site/src/modules/apps/apps.test.ts | 16 ++++++++++++++++ site/src/modules/apps/apps.ts | 20 +++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/site/src/modules/apps/apps.test.ts b/site/src/modules/apps/apps.test.ts index ed8d45825b4d9..e61b214a25385 100644 --- a/site/src/modules/apps/apps.test.ts +++ b/site/src/modules/apps/apps.test.ts @@ -53,6 +53,22 @@ describe("getAppHref", () => { expect(href).toBe(externalApp.url); }); + it("doesn't return the URL with the session token replaced when using unauthorized protocol", () => { + const externalApp = { + ...MockWorkspaceApp, + external: true, + url: `ftp://example.com?token=${SESSION_TOKEN_PLACEHOLDER}`, + }; + const href = getAppHref(externalApp, { + host: "*.apps-host.tld", + agent: MockWorkspaceAgent, + workspace: MockWorkspace, + path: "/path-base", + token: "user-session-token", + }); + expect(href).toBe(externalApp.url); + }); + it("returns a path when app doesn't use a subdomain", () => { const app = { ...MockWorkspaceApp, diff --git a/site/src/modules/apps/apps.ts b/site/src/modules/apps/apps.ts index b90f30fef96eb..3e67888b3b573 100644 --- a/site/src/modules/apps/apps.ts +++ b/site/src/modules/apps/apps.ts @@ -10,6 +10,20 @@ import type { // be used internally, and is highly subject to break. export const SESSION_TOKEN_PLACEHOLDER = "$SESSION_TOKEN"; +// This is a list of external app protocols that we +// allow to be opened in a new window. This is +// used to prevent phishing attacks where a user +// is tricked into clicking a link that opens +// a malicious app using the Coder session token. +export const ALLOWED_EXTERNAL_APP_PROTOCOLS = [ + "vscode:", + "vscode-insiders:", + "windsurf:", + "cursor:", + "jetbrains-gateway:", + "jetbrains:", +]; + type GetVSCodeHrefParams = { owner: string; workspace: string; @@ -78,7 +92,11 @@ export const getAppHref = ( { path, token, workspace, agent, host }: GetAppHrefParams, ): string => { if (isExternalApp(app)) { - return needsSessionToken(app) + const appProtocol = new URL(app.url).protocol; + const isAllowedProtocol = + ALLOWED_EXTERNAL_APP_PROTOCOLS.includes(appProtocol); + + return needsSessionToken(app) && isAllowedProtocol ? app.url.replaceAll(SESSION_TOKEN_PLACEHOLDER, token ?? "") : app.url; } From 7f897372870ddcc50201bdb5c99d55cb4ca417f7 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 9 May 2025 14:49:27 +0000 Subject: [PATCH 2/3] Remove unecessary export --- site/src/modules/apps/apps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/modules/apps/apps.ts b/site/src/modules/apps/apps.ts index 3e67888b3b573..a9b4ba499c17b 100644 --- a/site/src/modules/apps/apps.ts +++ b/site/src/modules/apps/apps.ts @@ -15,7 +15,7 @@ export const SESSION_TOKEN_PLACEHOLDER = "$SESSION_TOKEN"; // used to prevent phishing attacks where a user // is tricked into clicking a link that opens // a malicious app using the Coder session token. -export const ALLOWED_EXTERNAL_APP_PROTOCOLS = [ +const ALLOWED_EXTERNAL_APP_PROTOCOLS = [ "vscode:", "vscode-insiders:", "windsurf:", From bbfbf5ebf007b340b886ab14f578f248c2fb19d2 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 9 May 2025 14:59:30 +0000 Subject: [PATCH 3/3] Fix external app story --- site/src/modules/resources/AppLink/AppLink.stories.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/modules/resources/AppLink/AppLink.stories.tsx b/site/src/modules/resources/AppLink/AppLink.stories.tsx index 94cb0e2010b66..8f710e818aee2 100644 --- a/site/src/modules/resources/AppLink/AppLink.stories.tsx +++ b/site/src/modules/resources/AppLink/AppLink.stories.tsx @@ -80,6 +80,7 @@ export const ExternalApp: Story = { workspace: MockWorkspace, app: { ...MockWorkspaceApp, + url: "vscode://open", external: true, }, agent: MockWorkspaceAgent,