diff --git a/ui/src/ui/markdown.test.ts b/ui/src/ui/markdown.test.ts index 050dd52a166..a600eb49bd6 100644 --- a/ui/src/ui/markdown.test.ts +++ b/ui/src/ui/markdown.test.ts @@ -569,6 +569,20 @@ PY const html = toSanitizedMarkdownHtml("[click](file:///etc/passwd)"); expect(html).toBe("

click

\n"); }); + + it("strips href from host-local absolute file paths", () => { + const html = toSanitizedMarkdownHtml( + "[report.docx](/Users/test/.openclaw/data/skills/output/report.docx)", + ); + expect(html).toBe("

report.docx

\n"); + }); + + it("keeps app-relative links navigable", () => { + const html = toSanitizedMarkdownHtml("[usage](/usage)"); + expect(html).toBe( + '

usage

\n', + ); + }); }); describe("ReDoS protection", () => { diff --git a/ui/src/ui/markdown.ts b/ui/src/ui/markdown.ts index 7ec0461d28f..3f0d64ac197 100644 --- a/ui/src/ui/markdown.ts +++ b/ui/src/ui/markdown.ts @@ -84,6 +84,8 @@ const MARKDOWN_PARSE_LIMIT = 40_000; const MARKDOWN_CACHE_LIMIT = 200; const MARKDOWN_CACHE_MAX_CHARS = 50_000; const INLINE_DATA_IMAGE_RE = /^data:image\/[a-z0-9.+-]+;base64,/i; +const HOST_LOCAL_FILE_HREF_RE = + /^(?:~\/|\/(?:Users|home|tmp|private\/tmp|var\/folders|private\/var\/folders)\/|\/[A-Za-z]:\/|[A-Za-z]:[\\/])/; const markdownCache = new Map(); const TAIL_LINK_BLUR_CLASS = "chat-link-tail-blur"; @@ -135,6 +137,10 @@ function shouldRenderCodeBlockCopy(env: unknown): boolean { return (env as Partial | undefined)?.codeBlockChrome !== "none"; } +function isHostLocalFileHref(href: string): boolean { + return HOST_LOCAL_FILE_HREF_RE.test(href.trim()); +} + function installHooks() { if (hooksInstalled) { return; @@ -150,6 +156,11 @@ function installHooks() { return; } + if (isHostLocalFileHref(href)) { + node.removeAttribute("href"); + return; + } + // Block dangerous URL schemes (javascript:, data:, vbscript:, etc.) try { const url = new URL(href, window.location.href);