Rendering model output safely
Model output is untrusted input. It can contain HTML, scripts or malicious links - either by accident or because a prompt injection told the model to produce them. Rendering it with dangerouslySetInnerHTML is an XSS hole.
Bad
tsx
// marked() output goes straight into the DOM - <img onerror=...> runs
<div dangerouslySetInnerHTML={{ __html: marked(answer) }} />Good - markdown to React elements, raw HTML off
tsx
import ReactMarkdown from 'react-markdown';
<ReactMarkdown
skipHtml // ignore raw HTML in the markdown
components={{
a: ({ href, children }) => (
<a href={safeUrl(href)} target="_blank" rel="noopener noreferrer nofollow">{children}</a>
),
img: () => null, // or proxy / allowlist hosts
}}
>
{answer}
</ReactMarkdown>ts
const safeUrl = (href = '') => (/^(https?:|mailto:)/i.test(href) ? href : '#');If you must allow some HTML, sanitize with an allowlist (DOMPurify, rehype-sanitize) after converting.
Less obvious risks
- Data exfiltration via images - an injected
leaks data the moment the browser loads it. Block or proxy remote images. javascript:links - filter URL schemes, not just tags.- Code blocks - highlight as text; never
evalor auto-run generated code. Run it only in a sandbox (iframe withsandbox, a worker, a remote container). - Streaming - sanitize the rendered result, not each chunk; a tag split across chunks slips past chunk-level checks.
Defense in depth
A strict Content Security Policy (no inline scripts, limited img-src) catches what the renderer misses.
Rule: everything the model says is user input from a stranger. Render it the way you would render a comment on a public forum.
updated
did you know it?