iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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  -> &lt; 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…