mirror of
https://github.com/open-webui/open-webui.git
synced 2026-06-14 03:30:25 +00:00
fix: sanitize mermaid SVG output to prevent stored XSS in file preview (#25219)
renderMermaidDiagram returned raw mermaid SVG, which FilePreview.svelte injects via wrapper.innerHTML = svg. Mermaid runs with securityLevel: 'loose', so it neither sanitizes click hrefs (formatUrl skips sanitizeUrl) nor DOMPurifies its output; a .md file with a click X href "javascript:..." directive (or an HTML-label payload) therefore executes script in the app origin when previewed. The chat path was already safe because SVGPanZoom DOMPurifies before rendering; file preview was not. Sanitize at the source: renderMermaidDiagram now returns DOMPurify-cleaned SVG via a shared sanitizeSvg helper (same policy as SVGPanZoom), so every consumer including the FilePreview innerHTML sink receives safe output. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+40
-1
@@ -1,6 +1,7 @@
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import sha256 from 'js-sha256';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
@@ -1911,6 +1912,44 @@ const cleanupMermaidTempElements = (id: string) => {
|
||||
document.getElementById(`i${id}`)?.remove();
|
||||
};
|
||||
|
||||
// Mermaid runs with securityLevel:'loose', which emits unsanitized SVG (raw javascript: hrefs,
|
||||
// HTML labels); strip active content before it reaches any innerHTML/{@html} sink.
|
||||
export const sanitizeSvg = (svg: string): string =>
|
||||
DOMPurify.sanitize(svg, {
|
||||
USE_PROFILES: { svg: true, svgFilters: true },
|
||||
WHOLE_DOCUMENT: false,
|
||||
ADD_TAGS: ['style', 'foreignObject'],
|
||||
ADD_ATTR: [
|
||||
'class',
|
||||
'style',
|
||||
'id',
|
||||
'data-*',
|
||||
'viewBox',
|
||||
'preserveAspectRatio',
|
||||
'markerWidth',
|
||||
'markerHeight',
|
||||
'markerUnits',
|
||||
'refX',
|
||||
'refY',
|
||||
'orient',
|
||||
'href',
|
||||
'xlink:href',
|
||||
'dominant-baseline',
|
||||
'text-anchor',
|
||||
'clipPathUnits',
|
||||
'filterUnits',
|
||||
'patternUnits',
|
||||
'patternContentUnits',
|
||||
'maskUnits',
|
||||
'role',
|
||||
'aria-label',
|
||||
'aria-labelledby',
|
||||
'aria-hidden',
|
||||
'tabindex'
|
||||
],
|
||||
SANITIZE_DOM: true
|
||||
});
|
||||
|
||||
export const renderMermaidDiagram = async (
|
||||
mermaid: typeof import('mermaid').default,
|
||||
code: string,
|
||||
@@ -1921,7 +1960,7 @@ export const renderMermaidDiagram = async (
|
||||
const parseResult = await mermaid.parse(code, { suppressErrors: false });
|
||||
if (parseResult) {
|
||||
const { svg } = await mermaid.render(id, code);
|
||||
return svg;
|
||||
return sanitizeSvg(svg);
|
||||
}
|
||||
return '';
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user