Web UI: allow img tags in DOMPurify so markdown images render in webchat (#15480)

Thanks @lailoo.
This commit is contained in:
大猫子
2026-02-14 09:29:13 +08:00
committed by GitHub
parent 1d01bb1c8d
commit c4d2061a7c
3 changed files with 30 additions and 9 deletions

View File

@@ -82,6 +82,7 @@ Docs: https://docs.openclaw.ai
- Config: keep legacy audio transcription migration strict by rejecting non-string/unsafe command tokens while still migrating valid custom script executables. (#5042) Thanks @shayan919293.
- Status/Sessions: stop clamping derived `totalTokens` to context-window size, keep prompt-token snapshots wired through session accounting, and surface context usage as unknown when fresh snapshot data is missing to avoid false 100% reports. (#15114) Thanks @echoVic.
- Providers/MiniMax: switch implicit MiniMax API-key provider from `openai-completions` to `anthropic-messages` with the correct Anthropic-compatible base URL, fixing `invalid role: developer (2013)` errors on MiniMax M2.5. (#15275) Thanks @lailoo.
- Web UI: add `img` to DOMPurify allowed tags and `src`/`alt` to allowed attributes so markdown images render in webchat instead of being stripped. (#15437) Thanks @lailoo.
- Ollama/Agents: use resolved model/provider base URLs for native `/api/chat` streaming (including aliased providers), normalize `/v1` endpoints, and forward abort + `maxTokens` stream options for reliable cancellation and token caps. (#11853) Thanks @BrokenFinger98.
## 2026.2.12

View File

@@ -28,4 +28,24 @@ describe("toSanitizedMarkdownHtml", () => {
expect(html).toContain("<code");
expect(html).toContain("console.log(1)");
});
it("preserves img tags with src and alt from markdown images (#15437)", () => {
const html = toSanitizedMarkdownHtml("![Alt text](https://example.com/image.png)");
expect(html).toContain("<img");
expect(html).toContain('src="https://example.com/image.png"');
expect(html).toContain('alt="Alt text"');
});
it("preserves base64 data URI images (#15437)", () => {
const html = toSanitizedMarkdownHtml("![Chart](data:image/png;base64,iVBORw0KGgo=)");
expect(html).toContain("<img");
expect(html).toContain("data:image/png;base64,");
});
it("strips javascript image urls", () => {
const html = toSanitizedMarkdownHtml("![X](javascript:alert(1))");
expect(html).toContain("<img");
expect(html).not.toContain("javascript:");
expect(html).not.toContain("src=");
});
});

View File

@@ -33,9 +33,15 @@ const allowedTags = [
"thead",
"tr",
"ul",
"img",
];
const allowedAttrs = ["class", "href", "rel", "target", "title", "start"];
const allowedAttrs = ["class", "href", "rel", "target", "title", "start", "src", "alt"];
const sanitizeOptions = {
ALLOWED_TAGS: allowedTags,
ALLOWED_ATTR: allowedAttrs,
ADD_DATA_URI_TAGS: ["img"],
};
let hooksInstalled = false;
const MARKDOWN_CHAR_LIMIT = 140_000;
@@ -103,10 +109,7 @@ export function toSanitizedMarkdownHtml(markdown: string): string {
if (truncated.text.length > MARKDOWN_PARSE_LIMIT) {
const escaped = escapeHtml(`${truncated.text}${suffix}`);
const html = `<pre class="code-block">${escaped}</pre>`;
const sanitized = DOMPurify.sanitize(html, {
ALLOWED_TAGS: allowedTags,
ALLOWED_ATTR: allowedAttrs,
});
const sanitized = DOMPurify.sanitize(html, sanitizeOptions);
if (input.length <= MARKDOWN_CACHE_MAX_CHARS) {
setCachedMarkdown(input, sanitized);
}
@@ -115,10 +118,7 @@ export function toSanitizedMarkdownHtml(markdown: string): string {
const rendered = marked.parse(`${truncated.text}${suffix}`, {
renderer: htmlEscapeRenderer,
}) as string;
const sanitized = DOMPurify.sanitize(rendered, {
ALLOWED_TAGS: allowedTags,
ALLOWED_ATTR: allowedAttrs,
});
const sanitized = DOMPurify.sanitize(rendered, sanitizeOptions);
if (input.length <= MARKDOWN_CACHE_MAX_CHARS) {
setCachedMarkdown(input, sanitized);
}