Reject protocol-relative work product paths

This commit is contained in:
Dotta committed 2026-07-06 13:08:20 +00:00
1 parent 8074c5bd11
commit c84b3e7ca5
2 files changed
+23 -1

No files matched your search

@@ -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" }),
+1 -1
View File
@@ -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;
}