From ab5456eb1049aa8a0f3e51f359907ec7fff38b4b Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Fri, 6 Feb 2026 15:10:05 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20fix(security):=20sanitize=20AI-g?= =?UTF-8?q?enerated=20HTML=20to=20prevent=20XSS=20in=20playground?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mitigate XSS vulnerabilities in the playground where AI-generated content is rendered without sanitization, allowing potential script injection via prompt injection attacks. MarkdownRenderer.jsx: - Replace dangerouslySetInnerHTML with a sandboxed iframe for HTML preview - Use sandbox="allow-same-origin" to block script execution while allowing CSS rendering and iframe height auto-sizing - Add SandboxedHtmlPreview component with automatic height adjustment CodeViewer.jsx: - Add escapeHtml() utility to encode HTML entities before rendering - Rewrite highlightJson() to process tokens iteratively, escaping each token and structural text before wrapping in syntax highlighting spans - Escape non-JSON and very-large content paths that previously bypassed sanitization - Update linkRegex to correctly match URLs containing & entities These changes only affect the playground (AI output rendering). Admin- configured content (home page, about page, footer, notices) remains unaffected as they use separate code paths and are within the trusted admin boundary. --- .../common/markdown/MarkdownRenderer.jsx | 45 ++++++++++++++- web/src/components/playground/CodeViewer.jsx | 55 +++++++++++++------ 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/web/src/components/common/markdown/MarkdownRenderer.jsx b/web/src/components/common/markdown/MarkdownRenderer.jsx index 05419f8cc..6a71c695f 100644 --- a/web/src/components/common/markdown/MarkdownRenderer.jsx +++ b/web/src/components/common/markdown/MarkdownRenderer.jsx @@ -93,6 +93,49 @@ export function Mermaid(props) { ); } +function SandboxedHtmlPreview({ code }) { + const iframeRef = useRef(null); + const [iframeHeight, setIframeHeight] = useState(150); + + useEffect(() => { + const iframe = iframeRef.current; + if (!iframe) return; + + const handleLoad = () => { + try { + const doc = iframe.contentDocument || iframe.contentWindow?.document; + if (doc) { + const height = + doc.documentElement.scrollHeight || doc.body.scrollHeight; + setIframeHeight(Math.min(Math.max(height + 16, 60), 600)); + } + } catch { + // sandbox restrictions may prevent access, that's fine + } + }; + + iframe.addEventListener('load', handleLoad); + return () => iframe.removeEventListener('load', handleLoad); + }, [code]); + + return ( +