Skip to solution
mediumFrontend

How does React handle security concerns like XSS?

1.1k views
01

Understand 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.

securityxssjsxbest practices
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes basic JSX. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Every security behaviour below was produced by actually rendering the hostile input in

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

Escaped versus injected, and a blocked javascript: URL
Run Playground
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>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 31 of 119 decoded in the React.js track. One more won't hurt.

Back to track