diff --git a/packages/desktop/src-tauri/capabilities/default.json b/packages/desktop/src-tauri/capabilities/default.json index b5b023a..1f07204 100644 --- a/packages/desktop/src-tauri/capabilities/default.json +++ b/packages/desktop/src-tauri/capabilities/default.json @@ -8,8 +8,10 @@ { "identifier": "http:default", "allow": [ - { "url": "http://*" }, - { "url": "http://*:*" }, + { "url": "http://localhost" }, + { "url": "http://localhost:*" }, + { "url": "http://127.0.0.1" }, + { "url": "http://127.0.0.1:*" }, { "url": "https://*" }, { "url": "https://*:*" } ] diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json index 943f5af..1d67187 100644 --- a/packages/desktop/src-tauri/tauri.conf.json +++ b/packages/desktop/src-tauri/tauri.conf.json @@ -30,7 +30,7 @@ } ], "security": { - "csp": null + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' http://localhost:* http://127.0.0.1:* https:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" } }, "plugins": { diff --git a/packages/web/src/components/settings/SettingsForm.tsx b/packages/web/src/components/settings/SettingsForm.tsx index 1a767a5..066b5ac 100644 --- a/packages/web/src/components/settings/SettingsForm.tsx +++ b/packages/web/src/components/settings/SettingsForm.tsx @@ -24,6 +24,7 @@ import { isCloudInstance, } from "@/lib/config"; import { COLOR } from "@/lib/constants"; +import { tokenTransportError } from "@/lib/security"; export type ConnectionPreset = "cloud" | "self-hosted"; @@ -81,6 +82,13 @@ export function SettingsForm({ async function handleTest() { setChecking(true); setHealth({ status: "checking", message: "Connecting..." }); + const transportError = token.trim() ? tokenTransportError(baseUrl) : null; + if (transportError) { + setErrors({ baseUrl: transportError }); + setHealth({ status: "unreachable", message: transportError }); + setChecking(false); + return; + } const result = await checkConnection(baseUrl, token || undefined); setHealth(result); setChecking(false); @@ -112,6 +120,11 @@ export function SettingsForm({ setErrors({ token: "API key is required for Honcho Cloud" }); return; } + const transportError = token.trim() ? tokenTransportError(result.data.baseUrl) : null; + if (transportError) { + setErrors({ baseUrl: transportError }); + return; + } setErrors({}); let id: string; diff --git a/packages/web/src/components/workspaces/WebhookManager.tsx b/packages/web/src/components/workspaces/WebhookManager.tsx index e97c6f9..109b8c5 100644 --- a/packages/web/src/components/workspaces/WebhookManager.tsx +++ b/packages/web/src/components/workspaces/WebhookManager.tsx @@ -13,8 +13,12 @@ import { Input } from "@/components/ui/input"; import { Body, Muted, PageTitle, SectionHeading } from "@/components/ui/typography"; import { useDemo } from "@/hooks/useDemo"; import { COLOR } from "@/lib/constants"; +import { isHttpOrHttpsUrl, isSafeExternalUrl } from "@/lib/security"; -const urlSchema = z.string().url({ message: "Must be a valid URL" }); +const urlSchema = z + .string() + .url({ message: "Must be a valid URL" }) + .refine(isHttpOrHttpsUrl, { message: "Webhook URL must use http or https" }); interface Props { workspaceId: string; @@ -50,6 +54,15 @@ export function WebhookManager({ workspaceId }: Props) { setTimeout(() => setTestResult(null), 5000); }; + const handleOpenWebhook = async (webhookUrl: string) => { + if (!isSafeExternalUrl(webhookUrl)) { + setTestResult(`Blocked unsafe webhook URL: ${webhookUrl}`); + setTimeout(() => setTestResult(null), 5000); + return; + } + await open(webhookUrl); + }; + const list = Array.isArray(webhooks) ? webhooks : []; return ( @@ -161,7 +174,7 @@ export function WebhookManager({ workspaceId }: Props) {