The three variants

Defenses (in order)

  1. Context-aware output encoding. Encode user data for the exact HTML context it lands in:
    • Inside text → encode & < >
    • Inside an attribute → also encode quotes
    • Inside a URL → percent-encode
    • Inside a script context → don't do this. Re-architect.
  2. Use a templating engine that auto-escapes. Modern frameworks (React, Vue, Svelte, Django, Rails) escape by default. Treat any dangerouslySetInnerHTML / v-html / {{ raw }} as suspicious in code review.
  3. Sanitize untrusted HTML. If you must accept HTML from users, run it through a maintained sanitizer like DOMPurify with a strict allow-list.
  4. Set HttpOnly cookies. Even if XSS happens, scripts can't read session cookies. Doesn't prevent everything, but it cuts off the easiest impact.
  5. Content Security Policy. A strong CSP blocks inline scripts and limits what an injected script can reach. See CSP.
!

Don't trust the URL. URLs in href and src attributes can carry javascript: schemes. Always validate that user-supplied URLs use http://, https://, or your specific allow-list before placing them.