XSS, CSRF & CSP — Cheat Sheet

Lesson 19 · FE Lead Interview · 2026-06-12

XSS — 3 types

TypePayload livesFix point
ReflectedURL → responseServer output encoding
StoredDB → responseSanitize on input + encode on output
DOMURL hash / postMessage / localStorage → JS sinkClient-side: use textContent, DOMPurify

Defense layers (all required)

  1. Input sanitization — DOMPurify for HTML; reject otherwise
  2. Output encoding (context!) — HTML → entities; JS → \uXXXX; URL → percent-encode. Wrong context = bypass.
  3. HttpOnly cookies — limits blast radius (L18)
  4. CSP — blocks execution even if encoding fails

DOM sources & sinks

Sources: location.hash, location.search, document.referrer, postMessage, localStorage

Sinks: innerHTML, outerHTML, document.write, eval(), script.src, a.href="javascript:…"

CSP gold-standard header

Content-Security-Policy:
  script-src 'nonce-{r}' 'strict-dynamic'
             'unsafe-inline' http: https:;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self';
  connect-src 'self' https://api.example.com;
  report-uri /csp-violations;

'unsafe-inline' + http: are fallbacks for IE11 only — ignored by browsers that support 'strict-dynamic'.

Key directives

DirectiveProtects against
script-src 'nonce-…'XSS script execution
object-src 'none'Plugin/Flash XSS vectors
base-uri 'self'<base> injection → URL hijack
frame-ancestors 'self'Clickjacking (replaces X-Frame-Options)
connect-srcData exfiltration via fetch/XHR

Nonce vs Hash

NonceHash
Needs SSRYesNo
CDN static HTMLNoYes
Script changesFineRecompute
External src=YesNo

Clickjacking

Attack

Attacker loads your page in a transparent opacity:0 iframe over their own UI. Victim clicks attacker's button — actually clicks your page while logged in. Real credentials, real action, no XSS needed.

ValueUse for
frame-ancestors 'none'Payment, admin — never embeddable
frame-ancestors 'self'Most pages — blocks external attackers
frame-ancestors https://partner.comLegitimate cross-origin embed
Preconditions

Works when: no framing header, single-click action, victim logged in.

Blocked when: frame-ancestors 'none', action requires keyboard input, re-auth before sensitive step.

Defaults: Django → SAMEORIGIN; Spring Security → DENY; Helmet.js → SAMEORIGIN; Rails → none (manual). CSP frame-ancestors overrides X-Frame-Options in modern browsers.

strict-dynamic

Memorize

Propagates trust: scripts created by a nonce-trusted script inherit trust → lazy chunks and tag-manager sub-scripts execute without individual nonces.

Without it: your nonce-tagged bundle runs but import('./chunk') is blocked.

CSP rollout (always do this)

  1. Deploy Report-Only header
  2. Aggregate violations → triage (missing nonces vs real XSS)
  3. Fix all legitimate violations
  4. Wait 1–2 weeks of near-zero violations
  5. Flip to enforcing header, keep report-uri

CSRF (recap from L18)

Primary: SameSite=Lax — browser won't auto-send cookie on cross-site POSTs.

SameSite=None required? Add synchronizer token (X-CSRF-Token header) or double-submit cookie.

Remember: CSP does not prevent CSRF. Different attacks, different defenses.

Don't fail the interview

DOM XSS is invisible to the server

The fragment (#) is never sent. Server-side encoding does nothing. Fix is client-side: never write to innerHTML from attacker-controlled sources; use textContent or DOMPurify.

CSRF token does NOT prevent clickjacking

The request is real, credentialed, and valid. Only frame-ancestors stops it by preventing the iframe from rendering. X-Frame-Options is the legacy equivalent (SAMEORIGIN / DENY) — include it as fallback but prefer CSP.

unsafe-inline kills CSP

'unsafe-inline' in script-src allows injected scripts. Defeats the entire CSP. Remove it — add nonces to legitimate inline scripts instead.

Static nonce = no protection

Nonce must be cryptographically random and unique per request. A predictable or reused nonce is trivially bypassed.

Output encoding is context-dependent

HTML entity encoding inside a <script> block does not protect against XSS. Wrong context = bypass.