Skip to the solution
MediumFrontend6 min read

How does React handle security concerns like XSS?

1.1k views

The problem

Question presented to candidate: "How much XSS protection do you get from React for free, and where does it stop?"

What a strong answer should cover:

  • Auto-escaping: any value interpolated with {} is escaped to text, so it can never become markup. This covers the overwhelming majority of XSS risk.
  • Why it works: JSX produces elements as data, and React sets text via textContent-equivalent paths rather than parsing HTML.
  • dangerouslySetInnerHTML is the deliberate opt-out, named to create friction. It genuinely injects.
  • URL-based XSS: React 19 blocks javascript: URLs in href by rewriting them into a throwing expression. Older React only warned.
  • Still your responsibility: user-controlled URLs generally (validate the protocol), server-rendered data injected into the HTML shell, <script>/<style> content, and third-party markup.
  • Sanitise before rendering HTML you must render β€” DOMPurify β€” and prefer a Content-Security-Policy as defence in depth.
  • Server-side: escaping JSON embedded in the HTML document, and never trusting __html built from user input.

Clarifying questions expected:

  • "Are we rendering user-supplied HTML anywhere, or only text?"
  • "Is this client-rendered or server-rendered? SSR adds the HTML-shell injection surface."

Code / implementation expected: Optional. Showing the escaped-versus-dangerouslySetInnerHTML contrast makes the boundary concrete.

  • security
  • xss
  • jsx
  • best practices

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 8 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactEscaped versus injected, and a blocked javascript: URL
import { useState } from "react";

// A payload that tries to run code via an onerror handler.
const HOSTILE = '<img src=x onerror="document.title=\'pwned\'" alt="" />';

export default function App() {
  const [inspect, setInspect] = useState(null);

  const inspectHref = (e) => {
    // Read back what React ACTUALLY wrote to the DOM attribute.
    setInspect(e.currentTarget.getAttribute("href"));
    e.preventDefault();
  };

  const box = { border: "1px solid #ddd", borderRadius: 8, padding: 12, marginBottom: 12 };

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <div style={box}>
        <h4 style={{ marginTop: 0 }}>βœ… Interpolated β€” escaped to text</h4>
        {/* No element is created; the tags render as visible characters. */}
        <div style={{ fontFamily: "ui-monospace, monospace", fontSize: 12 }}>{HOSTILE}</div>
      </div>

      <div style={box}>
        <h4 style={{ marginTop: 0 }}>⚠️ dangerouslySetInnerHTML β€” genuinely injected</h4>
        {/* This creates a REAL img element with a live onerror handler.
            Never do this with unsanitised input. Run it through DOMPurify:
              import DOMPurify from "dompurify";
              __html: DOMPurify.sanitize(userHtml) */}
        <div dangerouslySetInnerHTML={{ __html: HOSTILE }} />
        <p style={{ fontSize: 13, color: "#a33", margin: "8px 0 0" }}>
          A broken-image icon above means a real element was created.
        </p>
      </div>

      <div style={box}>
        <h4 style={{ marginTop: 0 }}>πŸ›‘οΈ A javascript: URL in href</h4>
        <a href="javascript:document.title='pwned'" onClick={inspectHref}>
          click to inspect the rendered href
        </a>
        {inspect && (
          <pre style={{ whiteSpace: "pre-wrap", fontSize: 12, background: "#f6f6f6", padding: 8, borderRadius: 6 }}>
            {inspect}
          </pre>
        )}
        <p style={{ fontSize: 13, color: "#666", margin: "8px 0 0" }}>
          On React 19 the attribute is rewritten into a throwing expression, so
          the original code never runs. Validate protocols anyway.
        </p>
      </div>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 31 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

The `use()` API with Suspense β€” how do you unwrap promises in render?

Continue