Skip to content

Commit d25b493

Browse files
committed
fix: address markdown image review feedback
1 parent 4bf902d commit d25b493

4 files changed

Lines changed: 52 additions & 7 deletions

File tree

apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,22 @@ enum ChatMarkdownPreprocessor {
4040
if matches.isEmpty { return Result(cleaned: self.normalize(withoutTimestamps), images: []) }
4141

4242
var images: [InlineImage] = []
43-
var cleaned = withoutTimestamps
43+
let cleaned = NSMutableString(string: withoutTimestamps)
4444

4545
for match in matches.reversed() {
4646
guard match.numberOfRanges >= 3 else { continue }
4747
let label = ns.substring(with: match.range(at: 1))
4848
let source = ns.substring(with: match.range(at: 2))
4949

50-
let start = cleaned.index(cleaned.startIndex, offsetBy: match.range.location)
51-
let end = cleaned.index(start, offsetBy: match.range.length)
5250
if let inlineImage = self.inlineImage(label: label, source: source) {
5351
images.append(inlineImage)
54-
cleaned.replaceSubrange(start..<end, with: "")
52+
cleaned.replaceCharacters(in: match.range, with: "")
5553
} else {
56-
cleaned.replaceSubrange(start..<end, with: self.fallbackImageLabel(label))
54+
cleaned.replaceCharacters(in: match.range, with: self.fallbackImageLabel(label))
5755
}
5856
}
5957

60-
return Result(cleaned: self.normalize(cleaned), images: images.reversed())
58+
return Result(cleaned: self.normalize(cleaned as String), images: images.reversed())
6159
}
6260

6361
private static func inlineImage(label: String, source: String) -> InlineImage? {

apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatMarkdownPreprocessorTests.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ struct ChatMarkdownPreprocessorTests {
4242
#expect(result.images.isEmpty)
4343
}
4444

45+
@Test func handlesUnicodeBeforeRemoteMarkdownImages() {
46+
let markdown = "🙂![Leak](https://example.com/image.png)"
47+
48+
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
49+
50+
#expect(result.cleaned == "🙂Leak")
51+
#expect(result.images.isEmpty)
52+
}
53+
4554
@Test func stripsInboundUntrustedContextBlocks() {
4655
let markdown = """
4756
Conversation info (untrusted metadata):

src/auto-reply/reply/export-html/template.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,10 @@
665665
return div.innerHTML;
666666
}
667667

668+
function escapeHtmlAttr(text) {
669+
return escapeHtml(text).replaceAll('"', "&quot;").replaceAll("'", "&#39;");
670+
}
671+
668672
// Validate image fields before interpolating data URLs.
669673
const SAFE_IMAGE_MIME_RE = /^image\/(png|jpeg|gif|webp|svg\+xml|bmp|tiff|avif)$/i;
670674
const SAFE_BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
@@ -1725,7 +1729,7 @@
17251729
if (!INLINE_DATA_IMAGE_RE.test(href)) {
17261730
return escapeHtml(label);
17271731
}
1728-
return `<img src="${escapeHtml(href)}" alt="${escapeHtml(label)}">`;
1732+
return `<img src="${escapeHtmlAttr(href)}" alt="${escapeHtmlAttr(label)}">`;
17291733
}
17301734

17311735
// Configure marked with syntax highlighting and HTML escaping for text

src/auto-reply/reply/export-html/template.security.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,4 +284,38 @@ describe("export html security hardening", () => {
284284
expect(messages?.textContent).toContain("exfil");
285285
expect(messages?.querySelector(`img[src="${dataImage}"]`)).toBeTruthy();
286286
});
287+
288+
it("escapes markdown data-image attributes", () => {
289+
const dataImage = "data:image/png;base64,AAAA";
290+
const session: SessionData = {
291+
header: { id: "session-5", timestamp: now() },
292+
entries: [
293+
{
294+
id: "1",
295+
parentId: null,
296+
timestamp: now(),
297+
type: "message",
298+
message: {
299+
role: "assistant",
300+
content: [
301+
{
302+
type: "text",
303+
text: `![x" onerror="alert(1)](${dataImage})`,
304+
},
305+
],
306+
},
307+
},
308+
],
309+
leafId: "1",
310+
systemPrompt: "",
311+
tools: [],
312+
};
313+
314+
const { document } = renderTemplate(session);
315+
const img = document.querySelector("#messages img");
316+
expect(img).toBeTruthy();
317+
expect(img?.getAttribute("onerror")).toBeNull();
318+
expect(img?.getAttribute("alt")).toBe('x" onerror="alert(1)');
319+
expect(img?.getAttribute("src")).toBe(dataImage);
320+
});
287321
});

0 commit comments

Comments
 (0)