fix: enforce work product external url policy

This commit is contained in:
ZacharyZhang-NY committed 2026-04-23 20:54:39 -04:00
1 parent c779fc2858
commit 6c757f2081
7 files changed
+456 -30

No files matched your search

+14 -2
View File
@@ -1,6 +1,18 @@
import log from "electron-log/main.js";
import { z } from "zod";
import { requireParam, type AgentApiServer } from "./server-core";
import { normalizeExternalHttpUrl } from "../external-url-policy";
const workProductUrlSchema = z.string().trim().superRefine((value, ctx) => {
try {
normalizeExternalHttpUrl(value);
} catch (error) {
ctx.addIssue({
code: "custom",
message: error instanceof Error ? error.message : String(error),
});
}
}).transform((value) => normalizeExternalHttpUrl(value));
const createDocumentSchema = z.object({
type: z.string().optional(),
@@ -30,7 +42,7 @@ const createWorkProductSchema = z.object({
title: z.string(),
projectId: z.string().optional(),
externalId: z.string().optional(),
url: z.string().optional(),
url: workProductUrlSchema.optional(),
status: z.string().optional(),
reviewState: z.string().optional(),
isPrimary: z.boolean().optional(),
@@ -43,7 +55,7 @@ const updateWorkProductSchema = z.object({
provider: z.string().optional(),
externalId: z.string().optional(),
title: z.string().optional(),
url: z.string().optional(),
url: workProductUrlSchema.optional(),
status: z.string().optional(),
reviewState: z.string().optional(),
isPrimary: z.boolean().optional(),
+7 -26
View File
@@ -16,6 +16,7 @@ import {
import log from "electron-log/main.js";
import { attachWindowStatePersistence, loadWindowState } from "./window-state";
import type { NotificationOptions } from "./notification-service";
import { classifyExternalUrl } from "./external-url-policy";
export interface DesktopShellWindowDependencies {
preloadPath: string;
@@ -43,27 +44,6 @@ function isWithinDirectory(rootPath: string, targetPath: string) {
return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(`${normalizedRoot}${sep}`);
}
const TRUSTED_EXTERNAL_HOSTS = new Set([
"github.com",
"www.github.com",
"agentcompany.pages.dev",
"claude.ai",
"www.electronjs.org",
"www.electron.build",
]);
function shouldOpenTrustedExternalUrl(rawUrl: string) {
try {
const url = new URL(rawUrl);
if (url.protocol !== "https:" && url.protocol !== "http:") {
return false;
}
return TRUSTED_EXTERNAL_HOSTS.has(url.hostname.toLowerCase());
} catch {
return false;
}
}
export function createDesktopShellWindow(deps: DesktopShellWindowDependencies) {
const {
preloadPath,
@@ -172,11 +152,12 @@ export function createDesktopShellWindow(deps: DesktopShellWindowDependencies) {
});
app.on("web-contents-created", (_event, contents) => {
contents.setWindowOpenHandler(({ url }) => {
if (shouldOpenTrustedExternalUrl(url)) {
void shell.openExternal(url);
const decision = classifyExternalUrl(url);
if (decision.kind === "trusted") {
void shell.openExternal(decision.url);
return { action: "deny" };
}
if (url.startsWith("https://") || url.startsWith("http://")) {
if (decision.kind === "confirm") {
// Dialog is dispatched asynchronously so the main-process event loop
// is not blocked while the user decides. setWindowOpenHandler must
// return synchronously, so the window is always denied here; if the
@@ -190,13 +171,13 @@ export function createDesktopShellWindow(deps: DesktopShellWindowDependencies) {
cancelId: 1,
title: "Open External Link?",
message: "AgentCompany blocked an untrusted external link.",
detail: `${url}\n\nOnly trusted product and documentation domains open automatically. Review this destination before continuing.`,
detail: `${decision.url}\n\nOnly trusted product and documentation domains open automatically. Review this destination before continuing.`,
};
const result = parentWindow
? await dialog.showMessageBox(parentWindow, options)
: await dialog.showMessageBox(options);
if (result.response === 0) {
void shell.openExternal(url);
void shell.openExternal(decision.url);
}
})();
}
+54
View File
@@ -0,0 +1,54 @@
const TRUSTED_EXTERNAL_HOSTS = new Set([
"github.com",
"www.github.com",
"agentcompany.pages.dev",
"claude.ai",
"www.electronjs.org",
"www.electron.build",
]);
export type ExternalUrlDecision =
| { kind: "trusted"; url: string; host: string }
| { kind: "confirm"; url: string; host: string }
| { kind: "blocked"; reason: string };
export function normalizeExternalHttpUrl(rawUrl: string): string {
const trimmed = rawUrl.trim();
if (!trimmed) {
throw new Error("External URL is required.");
}
let parsed: URL;
try {
parsed = new URL(trimmed);
} catch {
throw new Error("External URL must be absolute.");
}
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
throw new Error("External URL protocol must be http or https.");
}
if (!parsed.hostname) {
throw new Error("External URL host is required.");
}
if (parsed.username || parsed.password) {
throw new Error("External URL credentials are not allowed.");
}
return parsed.toString();
}
export function classifyExternalUrl(rawUrl: string): ExternalUrlDecision {
let normalized: string;
try {
normalized = normalizeExternalHttpUrl(rawUrl);
} catch (error) {
return {
kind: "blocked",
reason: error instanceof Error ? error.message : String(error),
};
}
const parsed = new URL(normalized);
const host = parsed.hostname.toLowerCase();
if (TRUSTED_EXTERNAL_HOSTS.has(host)) {
return { kind: "trusted", url: normalized, host };
}
return { kind: "confirm", url: normalized, host };
}
+26 -2
View File
@@ -11,6 +11,31 @@ import {
} from "@shared/contracts";
import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common";
import { logAppError } from "./errors/index.js";
import { classifyExternalUrl } from "./external-url-policy";
async function openExternalUrlWithTrustPolicy(rawUrl: string) {
const decision = classifyExternalUrl(rawUrl);
if (decision.kind === "blocked") {
return fail("OPEN_EXTERNAL_URL_FORBIDDEN", decision.reason);
}
if (decision.kind === "confirm") {
const result = await dialog.showMessageBox({
type: "question",
buttons: ["Open Link", "Cancel"],
defaultId: 1,
cancelId: 1,
title: "Open External Link?",
message: "AgentCompany blocked an untrusted external link.",
detail: `${decision.url}\n\nOnly trusted product and documentation domains open automatically. Review this destination before continuing.`,
});
if (result.response !== 0) {
return ok(false);
}
}
const url = decision.url;
const error = await shell.openExternal(url).then(() => "").catch((err: unknown) => err instanceof Error ? err.message : String(err));
return error ? fail("OPEN_EXTERNAL_URL_FAILED", error) : ok(true);
}
export function registerCoreReadHandlers({
registerHandle,
@@ -85,8 +110,7 @@ export function registerCoreReadHandlers({
return fail(codeByReason[target.reason], target.detail);
}
if (target.kind === "url") {
const error = await shell.openExternal(target.value).then(() => "").catch((err: unknown) => err instanceof Error ? err.message : String(err));
return error ? fail("OPEN_PATH_FAILED", error) : ok(true);
return openExternalUrlWithTrustPolicy(target.value);
}
if (!db.core.settings.canOpenPath(target.value)) {
return fail("OPEN_PATH_FORBIDDEN", "The requested path is outside the managed desktop workspace.");