How It Works
How XSS works at a mechanism level — three classes (reflected, stored, DOM-based) — explained defensively so you can spot them in your own code.
XSS — mechanism, defensively
EXAMPLE
// SCOPE: defensive learning on the bundled lab app. Authorised testing only.
// Do not test against systems you do not own or have written permission to test.
// ===== The mechanism =====
// XSS is achieved when attacker-controlled data lands in a context where it is
// PARSED as code (HTML / JavaScript / event handlers) instead of treated as data.
//
// Three classes (Mitre / OWASP taxonomy):
// 1. Reflected — payload comes in a request and is reflected into the response
// 2. Stored — payload is persisted (DB, file) and rendered later to other users
// 3. DOM-based — JavaScript on the client builds DOM from untrusted input
// ===== Class 1: Reflected (illustrative bug, lab only) =====
// Server-rendered greeting page using a query param 'name' without escaping:
// /hello?name=<value>
// Vulnerable PHP (DO NOT do this in production):
// echo 'Hello, ' . $_GET['name'];
// Lab payload (in the bundled lab only):
// /hello?name=<script>document.title='lab'</script>
//
// Defensive fix:
// echo 'Hello, ' . htmlspecialchars($_GET['name'] ?? '', ENT_QUOTES | ENT_HTML5);
// ===== Class 2: Stored =====
// User posts a comment that is stored AS-IS and later rendered.
// Defensive fix:
// 1. Strip HTML at write time OR sanitise with an allowlist library (DOMPurify, sanitize-html, HTMLPurifier)
// 2. Default-escape at read time (template engine auto-escapes by default)
// 3. CSP nonce on inline scripts to block injected ones
// ===== Class 3: DOM-based =====
// JS on the client reads from location, document.referrer, postMessage, etc.,
// and injects into innerHTML / outerHTML / document.write.
// Example bad pattern:
// const q = new URLSearchParams(location.search).get('q');
// results.innerHTML = 'Results for: ' + q; // injection
// Defensive fix:
// results.textContent = 'Results for: ' + q; // safe
// Or use template literals + a sanitizer when you genuinely need rich text.
// ===== The 'context' problem =====
// Same payload behaves differently depending on where it lands:
// - HTML body -> < should be escaped
// - HTML attribute -> additional quoting concerns
// - JS string literal -> \ + Unicode escapes
// - URL -> percent-encode
// Defense: pick a templating layer that knows the CONTEXT (Blade auto-escapes for HTML;
// Mustache / Handlebars double-brace; JSX text auto-escapes).
// ===== Default-escape, then opt out =====
// Modern templates escape by default. Audit the OPT-OUT calls:
// Blade: {!! $value !!} (raw, dangerous)
// Vue: v-html
// React: dangerouslySetInnerHTML
// Twig: |raw
// Handlebars: {{{value}}}
// Every opt-out should:
// 1. Sanitise via an allowlist
// 2. Have a code review comment explaining why raw is required
// 3. Be paired with a CSP that mitigates the blast radius
// ===== Content Security Policy: the safety net =====
// Even with all the above, CSP is the difference between an injection and a breach.
// Strict CSP:
// Content-Security-Policy:
// default-src 'self';
// script-src 'self' 'nonce-<random>';
// object-src 'none'; base-uri 'self'; frame-ancestors 'none';
// Inline scripts must carry the nonce; anything else is rejected.
// ===== Cookies =====
// Mark session cookies HttpOnly + Secure + SameSite=Lax (or Strict).
// HttpOnly stops document.cookie exfiltration if XSS still happens.
// ===== Detection in code review =====
// Search for:
// - innerHTML / outerHTML / document.write / insertAdjacentHTML
// - {!! ... !!} (Blade), v-html (Vue), dangerouslySetInnerHTML (React)
// - Building HTML by concatenating strings
// - User input flowing into href / src without scheme validation
// ===== Patterns to internalise =====
// - Treat input as data until you have explicitly opted into HTML
// - Use templates with default escaping; review every opt-out
// - Strict CSP with nonces; never 'unsafe-inline' on script-src
// - HttpOnly + Secure + SameSite on session cookies
// - Lab + sanitize-html / DOMPurify when you genuinely need rich text
// ===== Pitfalls =====
// - Rolling your own sanitiser via regex; HTML is not regular
// - Allowing javascript: URLs in href
// - Storing rendered HTML and re-rendering raw 'because it's already safe'
// - Trusting markdown -> HTML output without sanitising
// - 'XSS only matters for logged-in pages' — public pages can host the staging payload
Why it matters
XSS is a templating problem dressed up as an attacker problem. Default-escape, sanitise with allowlists when you must render HTML, lock down with a strict CSP, and harden cookies. The three classes (reflected, stored, DOM) all collapse into the same rule: data should never be parsed as code.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Three common shapes: // Reflected: payload echoed back in the response (e.g. ?q=…). // Stored: payload saved server-side, then rendered later. // DOM-based: client-side JS reads attacker data (location, hash, postMessage) // and writes it into the DOM unsafely.Try it Yourself »
Discussion
Loading…