From c84b3e7ca57a9e8a6b6c7a932c9c4202a4368902 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 6 Jul 2026 13:08:20 +0000 Subject: [PATCH] Reject protocol-relative work product paths --- .../pages/ExecutionWorkspaceDetail.test.tsx | 22 +++++++++++++++++++ ui/src/pages/ExecutionWorkspaceDetail.tsx | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/ui/src/pages/ExecutionWorkspaceDetail.test.tsx b/ui/src/pages/ExecutionWorkspaceDetail.test.tsx index 22e68eb79c3..251dbbfe054 100644 --- a/ui/src/pages/ExecutionWorkspaceDetail.test.tsx +++ b/ui/src/pages/ExecutionWorkspaceDetail.test.tsx @@ -437,6 +437,28 @@ describe("ExecutionWorkspaceDetail plugin slots", () => { expect(container.querySelector('[data-testid="plugin-slot-mount"]')).toBeNull(); }); + it("does not render protocol-relative metadata work-product links", async () => { + mockRouteLocation.pathname = "/execution-workspaces/workspace-1/work-products"; + mockIssuesApi.list.mockResolvedValue([issue()]); + mockExecutionWorkspacesApi.get.mockResolvedValue(workspace({ + workProducts: [ + workProduct({ + id: "wp-file", + title: "agent-report.html", + metadata: { + openPath: "//evil.example/report.html", + }, + }), + ], + })); + + await render(); + + expect(container.textContent).toContain("agent-report.html"); + expect(container.querySelector('a[href="//evil.example/report.html"]')).toBeNull(); + expect(container.querySelector('a[href="https://evil.example/report.html"]')).toBeNull(); + }); + it("orders execution workspace plugin tabs against built-in tabs by slot order", async () => { mockPluginSlotState.slots = [ pluginSlot({ id: "default-tab", displayName: "Default" }), diff --git a/ui/src/pages/ExecutionWorkspaceDetail.tsx b/ui/src/pages/ExecutionWorkspaceDetail.tsx index 17cec596662..afebc017bd4 100644 --- a/ui/src/pages/ExecutionWorkspaceDetail.tsx +++ b/ui/src/pages/ExecutionWorkspaceDetail.tsx @@ -315,7 +315,7 @@ function safeWorkProductHref(workProduct: IssueWorkProduct) { const metadataOpenPath = metadata && typeof metadata.openPath === "string" ? metadata.openPath : null; const candidate = workProduct.url ?? metadataOpenPath; if (!candidate) return null; - if (candidate.startsWith("/")) return candidate; + if (candidate.startsWith("/") && !candidate.startsWith("//")) return candidate; return isSafeExternalUrl(candidate) ? candidate : null; }