diff --git a/.server-changes/sanitize-agent-view-urls.md b/.server-changes/sanitize-agent-view-urls.md new file mode 100644 index 00000000000..c534a03623d --- /dev/null +++ b/.server-changes/sanitize-agent-view-urls.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Sanitize URLs from streamed agent and tool data before rendering them in the dashboard's Agent view, so an unsafe scheme such as `javascript:` can no longer produce a clickable link or image source. diff --git a/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx b/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx index 6d3365752a6..fbd7faf2298 100644 --- a/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx +++ b/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx @@ -77,6 +77,27 @@ export const MessageBubble = memo(function MessageBubble({ return null; }); +// URLs in `source-url`/`file` parts come from streamed agent/tool data, so an +// unsafe scheme like `javascript:` would become a clickable XSS payload once it +// reaches an href/src. Allow only http(s)/blob (and data: for inline images), +// and return null for anything else so the caller can skip the link/image. +export function toSafeUrl(value: unknown, allowDataImage = false): string | null { + if (typeof value !== "string") return null; + let parsed: URL; + try { + parsed = new URL(value); + } catch { + return null; + } + if (parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.protocol === "blob:") { + return value; + } + if (allowDataImage && parsed.protocol === "data:" && /^data:image\//i.test(value)) { + return value; + } + return null; +} + export function renderPart(part: UIMessage["parts"][number], i: number) { const p = part as any; const type = part.type as string; @@ -159,15 +180,25 @@ export function renderPart(part: UIMessage["parts"][number], i: number) { // Source URL — clickable citation link if (type === "source-url") { + const safeUrl = toSafeUrl(p.url); + const label = p.title || p.url; + // Unsafe scheme: render the citation text without a clickable link. + if (!safeUrl) { + return label ? ( +