🔒 fix(security): sanitize AI-generated HTML to prevent XSS in playground

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.
This commit is contained in:
t0ng7u
2026-02-06 15:10:05 +08:00
parent a9982ef244
commit 708b7bef50
2 changed files with 83 additions and 17 deletions

View File

@@ -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 (
<iframe
ref={iframeRef}
sandbox='allow-same-origin'
srcDoc={code}
title='HTML Preview'
style={{
width: '100%',
height: `${iframeHeight}px`,
border: 'none',
overflow: 'auto',
backgroundColor: '#fff',
borderRadius: '4px',
}}
/>
);
}
export function PreCode(props) {
const ref = useRef(null);
const [mermaidCode, setMermaidCode] = useState('');
@@ -227,7 +270,7 @@ export function PreCode(props) {
>
HTML预览:
</div>
<div dangerouslySetInnerHTML={{ __html: htmlCode }} />
<SandboxedHtmlPreview code={htmlCode} />
</div>
)}
</>