How do you avoid common XSS pitfalls in modern frontend frameworks?
Default: let the framework escape ({value} in React, {{value}} in Vue/Angular). Never use dangerouslySetInnerHTML/v-html/innerHTML with untrusted content unless sanitized through DOMPurify with a strict allowlist. Validate URL schemes (block javascript:) before rendering as href/src. Never eval user input. Use CSP as defense-in-depth. For rich-text editors, sanitize at storage (not just display) and consider Trusted Types in Chromium.
XSS-safe rendering means user-controlled content can never become executable script in another user's browser. Two layers: avoid dangerous APIs by default, and sanitize when you must.
Rule 1: Trust the framework's escaping
Modern frameworks escape interpolated values automatically:
// React — escaped
<div>{userInput}</div><!-- Vue — escaped -->
<div>{{ userInput }}</div><!-- Angular — escaped -->
<div>{{ userInput }}</div>User submits <script>alert(1)</script> → renders as text (<script>...). Safe.
Rule 2: Don't reach for the dangerous APIs
React: dangerouslySetInnerHTML. Vue: v-html. Angular: [innerHTML]. Vanilla: innerHTML, outerHTML, document.write, insertAdjacentHTML.
Each of these bypasses escaping. If you write user input through them, that input becomes parsed HTML — including scripts.
If you don't need HTML output, use textContent / interpolation. Most content is plain text or markdown — render markdown via a library that emits safe HTML.
Rule 3: When you must render HTML, sanitize
For rich-text editor output, comment HTML, paste-from-Word content:
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ['p', 'br', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'h2', 'h3', 'blockquote', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'title'],
ALLOWED_URI_REGEXP: /^(?:https?:\/\/|mailto:|tel:|\/)/,
});
container.innerHTML = clean;DOMPurify is battle-tested. It handles weird edge cases — SVG <use> xlink:href injection, mutation XSS, attribute parser tricks — that a regex-based sanitizer would miss.
Sanitize at storage, not just at display. Otherwise, the next consumer (mobile app, RSS, export) gets raw HTML.
Rule 4: Validate URLs before using as href/src
Attacker injects <a href="javascript:alert(1)">. The framework's HTML escape doesn't help — the URL itself is the script.
function isSafeUrl(url: string): boolean {
try {
const u = new URL(url, window.location.origin);
return ['http:', 'https:', 'mailto:', 'tel:'].includes(u.protocol);
} catch { return false; }
}
<a href={isSafeUrl(url) ? url : '#'}>...</a>React 17+ warns on javascript: URIs but doesn't block them in all cases.
Rule 5: Never eval user input
eval(), new Function(), setTimeout('code', ...), setInterval('code', ...) — all parse strings as JS. Any user-controlled input there is RCE.
Same for <script> tags inserted via JS, even if you swear the content is "safe."
Rule 6: CSP as defense-in-depth
Even with perfect output handling, a regression slips through eventually. CSP limits damage:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';A successful XSS that injects <script>steal()</script> is blocked because the script lacks the nonce.
Rule 7: HttpOnly + Secure + SameSite cookies
Even if XSS happens, HttpOnly cookies aren't readable from JS — session token isn't stealable via document.cookie. Combined with short token lifetimes, the blast radius of XSS shrinks dramatically.
Rule 8: Trusted Types (Chromium)
Content-Security-Policy: require-trusted-types-for 'script'Forces dangerous DOM sinks (innerHTML, document.write) to require a TrustedHTML object. Plain strings are rejected at runtime. Eliminates entire classes of DOM-based XSS.
Rule 9: Watch out for less-obvious sinks
<style>content can includeexpression()in old IE (mostly historical).- SVG inside HTML can carry
<script>or event handlers. postMessagebetween windows — validate origin and structure.- WebSocket message rendering — same rules as HTTP.
- Markdown renderers: configure to strip raw HTML.
- Image
onerrorhandlers:<img src=x onerror=alert(1)>— never let user-supplied HTML through unsanitized.
Rule 10: Test it
- Manually try injecting
<img src=x onerror=alert(1)>andjavascript:alert(1)in all input fields. - Use ZAP, Burp, or similar to fuzz.
- Code review for any direct innerHTML / dangerouslySetInnerHTML usage.
- Add lint rule (
eslint-plugin-react/no-danger) to flag the dangerous API.
Pitfalls
- Trusting the framework but using innerHTML anyway.
- Sanitizing at display but not at storage — next renderer gets raw HTML.
- Regex-based "sanitizers" — always bypassed by clever payloads.
- Allowing href without scheme validation.
- Markdown renderers that pass HTML through by default.
- Server-side templates that auto-escape but you opt out for "safe" content.
- Trusting third-party widgets' HTML output without sanitization.
Mental model
XSS-safe rendering = let the framework escape by default; sanitize HTML with a battle-tested library when you must accept it; validate URL schemes; never eval. Layer with CSP, HttpOnly cookies, and Trusted Types as defense-in-depth.
Follow-up questions
- •Why use DOMPurify instead of a regex sanitizer?
- •How does Trusted Types prevent DOM XSS?
- •What's mutation XSS?
- •How do you safely render markdown?
Common mistakes
- •Using dangerouslySetInnerHTML / v-html / innerHTML with user input.
- •Regex-based 'sanitizers' — always bypassed.
- •Sanitizing at display only, not at storage.
- •Allowing href without scheme validation — javascript: injection.
- •eval() of user input.
- •Markdown renderer config that allows raw HTML.
Performance considerations
- •DOMPurify is fast (<1ms for typical inputs). Sanitization at storage time amortizes the cost across reads. CSP has zero runtime cost. The cost of a XSS incident dwarfs all these.
Edge cases
- •SVG embedded in HTML can carry scripts.
- •innerText is escape-safe; innerHTML is not.
- •postMessage from other windows — validate origin.
- •DOMParser('text/html') is safer for parsing but inserts none of the embedded scripts.
- •Mutation XSS: input that's safe-looking but mutates dangerously when parsed.
Real-world examples
- •GitHub, Stack Overflow — heavy use of DOMPurify-like sanitization for user content.
- •React's default escape behavior catches the most common cases.
- •OWASP Top 10 lists XSS year after year — it's still common.