From d25b493c7f79d13e7dca0278f66aeeb6c05f4d72 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sat, 7 Mar 2026 19:26:29 +0530 Subject: [PATCH] fix: address markdown image review feedback --- .../ChatMarkdownPreprocessor.swift | 10 +++--- .../ChatMarkdownPreprocessorTests.swift | 9 +++++ src/auto-reply/reply/export-html/template.js | 6 +++- .../export-html/template.security.test.ts | 34 +++++++++++++++++++ 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift index 746721e9d99..f03448140dc 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift @@ -40,24 +40,22 @@ enum ChatMarkdownPreprocessor { if matches.isEmpty { return Result(cleaned: self.normalize(withoutTimestamps), images: []) } var images: [InlineImage] = [] - var cleaned = withoutTimestamps + let cleaned = NSMutableString(string: withoutTimestamps) for match in matches.reversed() { guard match.numberOfRanges >= 3 else { continue } let label = ns.substring(with: match.range(at: 1)) let source = ns.substring(with: match.range(at: 2)) - let start = cleaned.index(cleaned.startIndex, offsetBy: match.range.location) - let end = cleaned.index(start, offsetBy: match.range.length) if let inlineImage = self.inlineImage(label: label, source: source) { images.append(inlineImage) - cleaned.replaceSubrange(start.. InlineImage? { diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatMarkdownPreprocessorTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatMarkdownPreprocessorTests.swift index 66b9d49fa8e..576e821c1e8 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatMarkdownPreprocessorTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatMarkdownPreprocessorTests.swift @@ -42,6 +42,15 @@ struct ChatMarkdownPreprocessorTests { #expect(result.images.isEmpty) } + @Test func handlesUnicodeBeforeRemoteMarkdownImages() { + let markdown = "🙂![Leak](https://example.com/image.png)" + + let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown) + + #expect(result.cleaned == "🙂Leak") + #expect(result.images.isEmpty) + } + @Test func stripsInboundUntrustedContextBlocks() { let markdown = """ Conversation info (untrusted metadata): diff --git a/src/auto-reply/reply/export-html/template.js b/src/auto-reply/reply/export-html/template.js index 521a183ea3c..da12d2625cf 100644 --- a/src/auto-reply/reply/export-html/template.js +++ b/src/auto-reply/reply/export-html/template.js @@ -665,6 +665,10 @@ return div.innerHTML; } + function escapeHtmlAttr(text) { + return escapeHtml(text).replaceAll('"', """).replaceAll("'", "'"); + } + // Validate image fields before interpolating data URLs. const SAFE_IMAGE_MIME_RE = /^image\/(png|jpeg|gif|webp|svg\+xml|bmp|tiff|avif)$/i; const SAFE_BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/; @@ -1725,7 +1729,7 @@ if (!INLINE_DATA_IMAGE_RE.test(href)) { return escapeHtml(label); } - return `${escapeHtml(label)}`; + return `${escapeHtmlAttr(label)}`; } // Configure marked with syntax highlighting and HTML escaping for text diff --git a/src/auto-reply/reply/export-html/template.security.test.ts b/src/auto-reply/reply/export-html/template.security.test.ts index aedf98c5986..9a42fd22337 100644 --- a/src/auto-reply/reply/export-html/template.security.test.ts +++ b/src/auto-reply/reply/export-html/template.security.test.ts @@ -284,4 +284,38 @@ describe("export html security hardening", () => { expect(messages?.textContent).toContain("exfil"); expect(messages?.querySelector(`img[src="${dataImage}"]`)).toBeTruthy(); }); + + it("escapes markdown data-image attributes", () => { + const dataImage = "data:image/png;base64,AAAA"; + const session: SessionData = { + header: { id: "session-5", timestamp: now() }, + entries: [ + { + id: "1", + parentId: null, + timestamp: now(), + type: "message", + message: { + role: "assistant", + content: [ + { + type: "text", + text: `![x" onerror="alert(1)](${dataImage})`, + }, + ], + }, + }, + ], + leafId: "1", + systemPrompt: "", + tools: [], + }; + + const { document } = renderTemplate(session); + const img = document.querySelector("#messages img"); + expect(img).toBeTruthy(); + expect(img?.getAttribute("onerror")).toBeNull(); + expect(img?.getAttribute("alt")).toBe('x" onerror="alert(1)'); + expect(img?.getAttribute("src")).toBe(dataImage); + }); });