DOM & Browsermedium
Safely render user-supplied HTML (sanitization)
You need to render user-authored markdown / rich text in a security console (analyst notes). Walk through a safe pipeline and the trust boundaries.
Asked at CrowdStrike, Google, Meta, Tenable
Answer
Trust boundaries:
- Input source: an authenticated analyst — still untrusted from the renderer's POV.
- Output target: the same domain as the auth cookie — XSS = full account takeover.
- Therefore: sanitize on render, every time, no exceptions.
Recommended pipeline:
- Parse markdown -> HTML with a strict parser (e.g.
markedorremark). Disable raw HTML pass-through if you don't need it. - Sanitize the HTML with DOMPurify configured to an allowlist:
import DOMPurify from 'dompurify'
const SAFE_CONFIG: DOMPurify.Config = {
ALLOWED_TAGS: [
'a','b','blockquote','br','code','em','h1','h2','h3','h4','hr',
'i','li','ol','p','pre','s','span','strong','table','tbody','td',
'th','thead','tr','ul'
],
ALLOWED_ATTR: ['href','title','class','rel','target'],
ALLOWED_URI_REGEXP: /^(?:https?:|mailto:|#|\/)/i,
FORBID_ATTR: ['style','onerror','onload','onclick'],
FORBID_TAGS: ['script','iframe','object','embed','form','input'],
}
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.tagName === 'A') {
node.setAttribute('rel', 'noopener noreferrer nofollow')
node.setAttribute('target', '_blank')
}
})
export function renderSafeHtml(dirtyHtml: string): string {
return DOMPurify.sanitize(dirtyHtml, SAFE_CONFIG)
}
- Inject with
dangerouslySetInnerHTML(React) orelement.innerHTML. The name of the React API is intentionally scary — keep the unsafe surface tiny.
function NoteBody({ markdown }: { markdown: string }) {
const html = useMemo(
() => renderSafeHtml(markdownToHtml(markdown)),
[markdown]
)
return <div className="note" dangerouslySetInnerHTML={{ __html: html }} />
}
Defense in depth (don't rely on one layer):
- CSP with no 'unsafe-inline' on script-src.
- Trusted Types (Chromium) so any string -> sink without policy throws.
- Server-side sanitize on write so a bug in the client doesn't taint storage forever.
Things that look safe but aren't:
- Stripping with regex. (Use a parser. Always.)
- Allowing
styleattributes (CSS injection -> data exfiltration via background-image). - Allowing
javascript:URLs in . URI regex must allowlist schemes. - Allowing
srcdocon iframes.