diff --git a/studio/frontend/src/components/assistant-ui/markdown-text.tsx b/studio/frontend/src/components/assistant-ui/markdown-text.tsx index fc547da0b1..ef9b33fced 100644 --- a/studio/frontend/src/components/assistant-ui/markdown-text.tsx +++ b/studio/frontend/src/components/assistant-ui/markdown-text.tsx @@ -13,7 +13,7 @@ import { HugeiconsIcon } from "@hugeicons/react"; import { createMathPlugin } from "@streamdown/math"; import { mermaid } from "@streamdown/mermaid"; import { useEffect, useMemo, useRef, useState } from "react"; -import { Block, type BlockProps, Streamdown } from "streamdown"; +import { Block, type BlockProps, Streamdown, defaultUrlTransform, type UrlTransform } from "streamdown"; import { createCodePlugin } from "./code-plugin"; import "katex/dist/katex.min.css"; import { AudioPlayer } from "./audio-player"; @@ -424,6 +424,22 @@ function useRafCoalescedText(text: string, isStreaming: boolean): string { return text; } +const safeImageUrl: UrlTransform = (url, _key, node) => { + // Only images are restricted; links/other nodes use the default transform. + if (node.tagName !== "img") return defaultUrlTransform(url, _key, node); + + // Strip ASCII controls first: browsers drop them mid-parse, so a value like + // "\t//attacker.com" would otherwise slip past the guards below. + // eslint-disable-next-line no-control-regex + const normalized = url.replace(/[\x00-\x1f\x7f]/g, "").trim(); + const lower = normalized.toLowerCase(); + + if (lower.startsWith("data:") || lower.startsWith("blob:")) return normalized; + if (/^[/\\]{2}/.test(normalized)) return null; // protocol-relative: // \\ /\ \/ + if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(normalized)) return null; // scheme prefix (colon later in path is fine) + return normalized; // relative -> same-origin +}; + const MarkdownTextImpl = () => { const { text, status } = useMessagePartText(); const displayText = useRafCoalescedText(text, status.type === "running"); @@ -444,6 +460,7 @@ const MarkdownTextImpl = () => { isAnimating={status.type === "running"} plugins={{ code, math, mermaid }} components={STREAMDOWN_COMPONENTS} + urlTransform={safeImageUrl} controls={{ code: false, mermaid: {