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

#security#XSS#sanitization#markdown

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:

  1. Parse markdown -> HTML with a strict parser (e.g. marked or remark). Disable raw HTML pass-through if you don't need it.
  2. 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)
}
  1. Inject with dangerouslySetInnerHTML (React) or element.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 style attributes (CSS injection -> data exfiltration via background-image).
  • Allowing javascript: URLs in . URI regex must allowlist schemes.
  • Allowing srcdoc on iframes.

Practise more DOM & Browser questions →