Skip to content
matthewcm.dev

    ↑ ↓ move · ↵ open · tab scopeopen graph →

    Preventing XSS in React

    React escapes what you put in JSX, so most XSS comes through the escape hatches — dangerouslySetInnerHTML, direct DOM writes, javascript URLs and third-party scripts. Defend in layers.

    Evergreentended planted 2 min read
    On this page

    Cross-site scripting (XSS) is getting an attacker’s script to run in your users’ browsers, with your users’ sessions. React protects you more than older approaches did — but only on the paved road. Step off it and you’re on your own.

    What React does for you

    Anything you interpolate in JSX is escaped. {userInput} renders as text even if it contains <script> tags. That one default removes the most common class of XSS bugs.

    Where XSS still gets in

    • dangerouslySetInnerHTML. The name is the warning. If HTML is unavoidable (rendered Markdown, a CMS), sanitise it with a well-maintained library such as DOMPurify. Don’t write your own sanitiser.
    • Direct DOM writes. innerHTML, insertAdjacentHTML, or appending HTML strings via refs bypass React entirely. Use textContent if you must touch the DOM.
    • javascript: URLs. <a href={user.website}> happily renders javascript:alert(1). Validate URL schemes:
    const safeUrl = (u: string) => (/^https?:\/\//i.test(u) ? u : "#");
    <a href={safeUrl(user.website)}>site</a>;
    • Spreading user-controlled props onto elements, which can smuggle in event handlers or hrefs.
    • Server-rendered state. Serialising state into a <script> tag with plain JSON.stringify lets a </script> in the data break out. Escape <.
    • Third-party scripts. Every tag manager and widget runs with full access to your page.

    Defence in layers

    1. Validate data at the boundary — everything from servers and third-party APIs (API Modelling).
    2. Render through JSX and avoid the escape hatches.
    3. Sanitise when you can’t avoid HTML.
    4. Content-Security-Policy. A strict CSP (script-src 'self', nonces for inline scripts, object-src 'none') stops injected scripts running even if one slips through. This is the layer that saves you from the bug you didn’t find.
    5. HttpOnly cookies so a successful XSS can’t read session tokens.
    6. Trusted Types, where supported, make the browser reject raw strings assigned to dangerous DOM sinks.

    In practice

    Every XSS finding I’ve seen in a React codebase came from an escape hatch added “just this once” — usually rendering HTML from an API or wiring a URL from user data. A lint rule that flags dangerouslySetInnerHTML and a CSP header in the deployment are cheap insurance. Security belongs early in the pipeline alongside tests — see Shift Left Testing — and leaked secrets deserve the same treatment (Removing Secrets from Git History).

    • #security
    • #xss
    • #react
    • #csp

    ↪ Links to (3)