Lesson 19 · FE Lead Interview · 2026-06-12
| Type | Payload lives | Fix point |
|---|---|---|
| Reflected | URL → response | Server output encoding |
| Stored | DB → response | Sanitize on input + encode on output |
| DOM | URL hash / postMessage / localStorage → JS sink | Client-side: use textContent, DOMPurify |
\uXXXX; URL → percent-encode. Wrong context = bypass.Sources: location.hash, location.search, document.referrer, postMessage, localStorage
Sinks: innerHTML, outerHTML, document.write, eval(), script.src, a.href="javascript:…"
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'.
| Directive | Protects 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-src | Data exfiltration via fetch/XHR |
| Nonce | Hash | |
|---|---|---|
| Needs SSR | Yes | No |
| CDN static HTML | No | Yes |
| Script changes | Fine | Recompute |
| External src= | Yes | No |
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.
| Value | Use for |
|---|---|
frame-ancestors 'none' | Payment, admin — never embeddable |
frame-ancestors 'self' | Most pages — blocks external attackers |
frame-ancestors https://partner.com | Legitimate cross-origin embed |
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.
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.
Report-Only headerreport-uriPrimary: 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.
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.
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' in script-src allows injected scripts. Defeats the entire CSP. Remove it — add nonces to legitimate inline scripts instead.
Nonce must be cryptographically random and unique per request. A predictable or reused nonce is trivially bypassed.
HTML entity encoding inside a <script> block does not protect against XSS. Wrong context = bypass.